Skip to content

Add block search command - #16

Open
TeemuSuoranta wants to merge 11 commits into
wp-cli:mainfrom
TeemuSuoranta:add/block-search-command
Open

TeemuSuoranta wants to merge 11 commits into
wp-cli:mainfrom
TeemuSuoranta:add/block-search-command

Conversation

@TeemuSuoranta

Copy link
Copy Markdown

Summary

This PR adds a new wp block search command to the block-command package.

The command searches posts for block usage across parsed block trees, including nested blocks, and supports multiple filter types:

  • exact block name with --block
  • block namespace with --block-namespace
  • block style with --style
  • embedded pattern name with --pattern
  • embedded pattern namespace with --pattern-namespace
  • synced pattern reference by reusable block post ID with --synced-pattern

The command returns matching posts and supports the standard WP-CLI output formats such as table, json, csv, yaml, count, and ids.

Why

Block usage is a common operational and migration concern in block-based sites.

Typical use cases include:

  • finding all content that uses a specific block before changing or removing it
  • performing design review for blocks or patterns based on real content
  • finding out obsolete blocks, patterns or block styles
  • identifying posts using a specific block style such as is-style-rounded
  • locating content that references a specific synced pattern / reusable block
  • narrowing searches to a post type, post status, ID set, or other WP_Query constraints
  • exporting results for downstream reporting or scripted migrations

Before this change, users could inspect blocks indirectly, but there was no dedicated command in block-command for querying content by block usage.

Command Design

The command lives under wp block search, which is the natural home for block-related content inspection. Alternative option was under wp post but as that is more directed on handling singular post data, this is more fitting.

Supported filters:

  • --block=<name> for exact block matching such as core/image
  • --block-namespace=<namespace> for namespace-level searches such as core
  • --style=<style-name> for block style matching such as outline
  • --pattern=<pattern-name> for exact embedded pattern matching via metadata.patternName such as twentytwentyfive/event-rsvp
  • --pattern-namespace=<namespace> for embedded pattern namespace matching such as twentytwentyfive
  • --synced-pattern=<post-id> for core/block references pointing at a reusable block / synced pattern ID

Validation rules:

  • at least one search filter is required
  • --block and --block-namespace are mutually exclusive
  • --pattern and --pattern-namespace are mutually exclusive
  • --synced-pattern must be a positive integer

Technical Choices And Rationale

Parse blocks instead of relying on raw SQL or string matching

The command uses parse_blocks() and walks the parsed tree rather than trying to inspect post content with string matching or SQL.

Rationale:

  • block structure is nested and semantic
  • styles and metadata live in parsed attributes, not only in plain text patterns
  • the command needs accurate occurrence counting
  • the command needs to support combinations of block, style, pattern, and synced-pattern filters

Exception:

  • For searching only with block filter, we use has_block() that avoids running parse_blocks() for better performance.

Keep pattern matching and synced pattern matching separate

Embedded patterns and synced patterns are different concepts and are stored differently.

  • embedded patterns are detected via block metadata, specifically metadata.patternName
  • synced patterns are detected via core/block with a ref attribute pointing to a wp_block post

Rationale:

  • avoids overloading a single flag with multiple meanings
  • keeps the command contract explicit for users
  • makes validation and implementation easier to reason about

Preserve nearest pattern ancestor context when flattening nested blocks

The command flattens parsed blocks for matching, but nested block searches need pattern ancestry to remain available.

Example:

  • a wrapper block may carry metadata.patternName
  • a child block may carry the actual blockName or style being searched

Without inherited ancestor pattern context, a combined search like --pattern-namespace=twentytwentyfive --block=core/image would fail for nested images inside a pattern wrapper.

The implementation therefore carries the nearest ancestor pattern name onto descendants during flattening.

Rationale:

  • fixes combined block + pattern searches for nested content
  • correctly handles patterns nested inside patterns by preferring the nearest ancestor
  • keeps the later matcher simple and local per flattened node

Add a cheap has_block() fast path for exact block-only searches

For the narrow case where the search is only --block=<exact-name>, the command uses has_block() as a cheap precheck before parsing blocks.

Rationale:

  • avoids unnecessary parse_blocks() work for posts that clearly do not contain the requested block
  • does not affect more complex searches involving namespace, style, pattern, or synced-pattern filters
  • keeps the optimization safe by limiting it to the exact block-only case

Use constrained WP_Query defaults for large scans

The command uses:

  • no_found_rows => true
  • cache_results => false
  • update_post_meta_cache => false
  • update_post_term_cache => false

Rationale:

  • reduces overhead for read-only search workloads
  • avoids unnecessary object cache churn when scanning many posts
  • fits the command’s usage pattern, where result hydration beyond the loaded posts is not needed

Usage Examples

Find posts using a specific block:

wp block search --block=core/paragraph

Find posts using any block from a namespace:

wp block search --block-namespace=core

Limit the search to a specific post type:

wp block search --block=core/image --post_type=page

Find posts using a specific style:

wp block search --style=rounded

Combine block and style filters:

wp block search --block=core/image --style=rounded

Find posts using blocks embedded from a specific pattern:

wp block search --pattern=twentytwentyfive/event-rsvp

Find posts using blocks from patterns in a namespace:

wp block search --pattern-namespace=twentytwentyfive

Find images that appear inside a specific pattern namespace:

wp block search --pattern-namespace=twentytwentyfive --block=core/image

Find posts that reference a synced pattern / reusable block:

wp block search --synced-pattern=123

Return JSON output for automation:

wp block search --block=core/heading --post_status=publish --fields=ID,post_type,occurrences --format=json

Return permalinks for matching posts:

wp block search --block=core/quote --fields=url --format=csv

Return only the count:

wp block search --style=rounded --format=count

Testing

Acceptance coverage was added for:

  • exact block matching
  • namespace matching
  • nested block matching
  • style matching
  • count output
  • IDs output
  • JSON output with occurrences
  • exact embedded pattern matching
  • embedded pattern namespace matching
  • combined pattern + block matching
  • nearest-ancestor behavior for patterns nested inside patterns
  • synced pattern reference matching
  • validation errors for incompatible or missing parameters

Notes

  • metadata.patternName has not been around in patterns since their introduction. This means pattern search may not find all patterns where they are added before core introduced this meta.
  • --synced-pattern is intentionally exact-only and currently accepts a single post ID.
  • pattern filters and synced pattern filters are intentionally separate to keep the CLI contract clear.
  • the command is designed for accuracy first, with scoped optimizations where they are safe.

Related issues

Acknowledgements

Thanks to @janw-me and @schlessera for guidance during WCEU 2026. The scope, naming and structure was refined greatly with them.

@TeemuSuoranta
TeemuSuoranta requested a review from a team as a code owner June 5, 2026 05:23
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Hello! 👋

Thanks for opening this pull request! Please check out our contributing guidelines. We appreciate you taking the initiative to contribute to this project.

Contributing isn't limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation.

Here are some useful Composer commands to get you started:

  • composer install: Install dependencies.
  • composer test: Run the full test suite.
  • composer phpcs: Check for code style violations.
  • composer phpcbf: Automatically fix code style violations.
  • composer phpunit: Run unit tests.
  • composer behat: Run behavior-driven tests.

To run a single Behat test, you can use the following command:

# Run all tests in a single file
composer behat features/some-feature.feature

# Run only a specific scenario (where 123 is the line number of the "Scenario:" title)
composer behat features/some-feature.feature:123

You can find a list of all available Behat steps in our handbook.

@github-actions github-actions Bot added command:block Related to 'block' command documentation Improvements or additions to documentation enhancement New feature or request labels Jun 5, 2026
@TeemuSuoranta

Copy link
Copy Markdown
Author

I'll keep working on this and try to get test notices fixed during WCEU.

@codecov

codecov Bot commented Jun 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.78313% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/Block_Search_Command.php 95.78% 14 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new wp block search command to the block-command package to locate posts by block usage (including nested blocks) with support for filters like block name/namespace, style, embedded pattern name/namespace, and synced pattern references.

Changes:

  • Added wp block search command implementation that walks parsed block trees and reports matching posts with occurrence counts.
  • Registered the new subcommand and updated package metadata/documentation to expose it in help/README.
  • Added Behat coverage for the command’s filters, combinations, and validation rules.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/Block_Search_Command.php Implements the new wp block search command (querying posts, parsing blocks, matching filters, formatting output).
src/Block_Command.php Adds an example for wp block search to the namespace command help text.
README.md Documents wp block search usage, options, fields, and examples.
features/block-search.feature Adds acceptance tests for block/style/pattern/synced-pattern search behaviors and validation errors.
composer.json Registers block search in the commands list and adds a dev dependency used by tests.
block-command.php Registers the block search command with WP-CLI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Block_Search_Command.php Resolved
Comment thread src/Block_Search_Command.php Resolved
Comment thread README.md Resolved
Comment thread src/Block_Search_Command.php Resolved
Comment thread src/Block_Command.php Resolved
@TeemuSuoranta

TeemuSuoranta commented Jun 5, 2026 •

Copy link
Copy Markdown
Author

@swissspidy Copilot suggestions done. The -1 in posts_per_page mitigated to reasonable degree with adding post_content prefiltering to get most obvious non-matches out as early as possible. IMO pagination is not practical solution here as it makes search very cumbersome to use and hard to indicate when there is more content in next page. I think we are doing everything we can in current DB structure.

@swissspidy

Copy link
Copy Markdown
Member

Yeah -1 is one aspect here I'm skeptical about. A command like this would need to be runnable even on the largest of sites, one way or another. Is it possible for users to enable pagination if needed, like wp post list does for example?

@TeemuSuoranta

Copy link
Copy Markdown
Author

Yeah -1 is one aspect here I'm skeptical about. A command like this would need to be runnable even on the largest of sites, one way or another. Is it possible for users to enable pagination if needed, like wp post list does for example?

I can test and document paginated use. We should be getting WP_Query's native pagination via already supported parameters but good idea to test it and document its usage.

Not sure if there is any good way to hint to user how many pages but if you keep going through items like 1000 item chunks and stop until you get nothing back that could work for large sites that cannot do this otherwise.

Some sort of index or database table for this would of course be much more performant but that is so big of an ask that it will never be done for a feature like this. I'll look into paginated use and get back.

@TeemuSuoranta

Copy link
Copy Markdown
Author

Pagination is possible through native posts_per_page and paged. Tested and documented that. Also added a test for it.

To be noted though that posts per page is more like defining the search area and not directly how many results you are going to get per page.

That is because we cannot for sure retrieve only posts that have matches from WP_Query. We do the prefilter and like simple block search might be pretty accurate there might be more false positives on some others. All false positives are cleared in actual filtering with parse_blocks() processing.

I wouldn't simplify the scope to get prefilter accurate because there's still some potential more filters that are even less SQL like/regex compatible. Mainly thinking about like arbitary attribute/value filter for blocks.

But those would be later additions if real use cases emerge. It's very doable but requires some extensive tests.

@swissspidy
swissspidy requested a review from schlessera August 9, 2026 21:04

@schlessera schlessera left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, and for working through the earlier feedback. Good command, right home under wp block, and parse-then-match is the correct design; the prefilter and the style token check were the right answers to the first round.

Two things block it, both as inline comments: the branch has to be rebased on main (it still requires the 2.x framework, hence the conflict), and the memory profile on large sites (posts_per_page => -1) needs internal chunking rather than user-side paging. The other inline comments are small: pattern occurrence counts, --block=paragraph, the synced-pattern combinations, the integer check, the permalink cost, the README hand edits.

Milestone 1.1.0: a new command is a minor bump. The milestone does not exist yet; I will create it.

Comment thread composer.json Resolved
Comment thread composer.json Resolved
Comment thread src/Block_Search_Command.php Resolved
Comment thread src/Block_Search_Command.php Resolved
Comment thread src/Block_Search_Command.php Resolved
Comment thread src/Block_Search_Command.php Resolved
Comment thread src/Block_Search_Command.php Resolved
Comment thread src/Block_Search_Command.php Resolved
Comment thread src/Block_Search_Command.php Resolved
Comment thread README.md Resolved
@schlessera schlessera added this to the 1.1.0 milestone Sep 25, 2026
@TeemuSuoranta

Copy link
Copy Markdown
Author

Thanks for the review! I'll get through these in coming weeks when I have time. I'll see this through and won't leave you hanging!

@TeemuSuoranta
TeemuSuoranta force-pushed the add/block-search-command branch from 077bad9 to cac5df0 Compare October 10, 2026 14:50
@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7ce96cd3-469d-4761-ac9e-afffe7442716

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@TeemuSuoranta

Copy link
Copy Markdown
Author

Thanks for the thorough review, @schlessera. I've addressed all of it, and used the opportunity to tighten performance and fix a few edge cases I found while testing. Summary below, with the reasoning for the choices that weren't obvious.

It's a lot of text but I wanted to put all changes explicitly with reasoning here.

Branch and dependencies

  • Rebased on main, with wp-cli/wp-cli: ^3.0 kept.
  • entity-command and extension-command are ^2 || ^3, matching extension-command on main.

Memory on large sites (blocking)

  • The command no longer loads every candidate post at once. It runs the prefiltered query with fields => 'ids', then loads and parses posts in batches of 200 (BATCH_SIZE).
  • Memory stays flat. The only things held across the whole run are the ID list and one small row per match.
  • Output is complete by default, so users don't have to page to avoid memory problems. I removed the docblock note about pages coming back unequal or empty.
  • I went with your ids-first suggestion over paging full-post queries with paged. It avoids OFFSET slowdown on later pages and avoids skipped or duplicated posts if content changes mid-run.
  • The batch query keeps cache_results => false. Caching would make the in-memory cache grow with the number of candidates and undo the memory fix.
  • Explicit --posts_per_page and --paged still work and select exactly that window of IDs. Each page is an independent run, and page size counts candidate posts, not matches.
  • When no orderby is passed, the ID query orders by ID ASC. Without a tiebreaker, posts sharing a post_date (common after imports) could be skipped or repeated between pages.
  • --offset without a page size now works. posts_per_page => -1 makes WP_Query ignore offset, so an effectively unbounded size is used in that case.

Correctness

  • --block=paragraph is now normalised to core/paragraph, consistent with has_block().
  • --synced-pattern must be a positive integer (digits only), so 12abc errors.
  • --synced-pattern cannot be combined with --block or --block-namespace. It now errors like the other mutually exclusive pairs.
  • --pattern counts only blocks carrying their own metadata.patternName, so occurrences is the number of insertions rather than the number of inner blocks. It's asserted in the pattern scenarios.
  • get_permalink() runs only when url is requested.

Performance

  • A plain --block search no longer calls parse_blocks() unless occurrences is requested. The SQL prefilter plus has_block() already decide the match, and has_block() checks the trailing space, so my-plugin/prefix doesn't match my-plugin/prefix-extra. occurrences is now present in the output only when requested.
  • For other filters (namespace, style, pattern, synced pattern), matching stops at the first hit when occurrences isn't requested.
  • Block trees are walked lazily with a generator instead of building a flattened copy of the whole tree.
  • The pattern prefilters use a "patternName" guard plus the escaped name as separate LIKEs, so any JSON spacing matches.
  • The synced-pattern prefilter uses a "ref" guard plus a REGEXP that allows any whitespace and rejects longer IDs (ref 5151 does not match 51510). The bare ID can't be matched with LIKE alone, because it would match almost every post.
  • Prefilter markers now allow for how content is really stored: attribute values are escaped the way the serializer does it (&, <, >, --), and wp:core/image written explicitly is found as well as wp:image.
  • If the database rejects REGEXP, the synced-pattern prefilter falls back to LIKE on the compact and single-space forms. In that fallback, JSON with other spacing would be excluded by the prefilter.
  • The REGEXP path was verified on sqlite only. MySQL and MariaDB use different regex engines, so CI is the first real check there. The fallback path was not exercised, because sqlite accepted REGEXP.

Docs

  • The docblock now states that wp_block and other non-searchable post types are excluded by default, and that blocks living only inside a synced pattern aren't found through core/block references.
  • I couldn't run wp scaffold package-readme in my environment, so I updated the README by hand to match the docblock. Please regenerate it if CI flags a difference.

Tests

  • New or updated scenarios cover the synced-pattern validation and combinations, --block=paragraph, permalink-on-demand, pattern occurrences, a scan over more than one batch (205 posts), stable pagination for posts sharing a date, --offset, the name-prefix case for the no-parse path, explicit wp:core/ names, spaced JSON attributes (including unusual whitespace), escaped style names, and --limit (stops at the limit, validation, and the --paged conflict).
  • features/block-search.feature passes locally (30 of 30 scenarios).
  • phpcs is clean for the changed file. phpstan shows the same 7 errors as before my changes, none in the search command.
  • I ran the tests on sqlite because there was no MySQL client, and I couldn't run composer lint-gherkin locally. CI will cover both.

--limit

  • I added --limit=<n>, which stops after n matching posts, in query order.
  • Why: --posts_per_page / --paged / --offset limit candidate posts, not matches. In wp post list a returned row is a matched row, but here a candidate only becomes a match after its blocks are parsed, so a page of 50 can contain fewer than 50 matches or none. --limit counts matches, and batches that aren't needed aren't loaded.
  • It must be a positive integer, and it can't be combined with --paged, because the combination is ambiguous.
  • None of the other WP-CLI commands I checked has --limit (wp post list uses --posts_per_page), so this is a small new convention which made me a bit unsure if this was right call. I think it is reasonable to have as it's once more performance aspect that it will exit early the parsing loop when set. It's easy to remove if you don't feel it should be there.

I think this is going to be really good command to have and very useful for AI coding agents too. I have a few additional ideas already in mind but I'd freeze the features for now and get this shipped first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

command:block Related to 'block' command documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants