Skip to content

fix: write child posts to their canonical hierarchical path - #405

Open
chubes4 wants to merge 1 commit into
mainfrom
fix/native-hierarchical-post-paths
Open

fix: write child posts to their canonical hierarchical path#405
chubes4 wants to merge 1 commit into
mainfrom
fix/native-hierarchical-post-paths

Conversation

@chubes4

@chubes4 chubes4 commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Two independent defects in the native runtime, both of which make a post's body invisible to queries. Found while a wiki article written to wordpress-com/infrastructure/ landed flat at the wiki root and then matched no content search at all — not even LIKE '%'.

1. The native runtime has no post resolver

WP_Markdown_Native_Runtime_Factory::shared_storage() constructs WP_Markdown_Storage and never calls set_post_resolver(). Every other backend wires one — class-wp-markdown-db.php, the CLI, the primary storage runtime, the reconciliation adapter.

resolve_parent_dir() needs that resolver to walk a post's ancestor chain:

if ( 0 === $parent_id || null === $this->post_resolver ) {
    return $type_dir;
}

With no resolver it returns the post-type root on the first line, so a child post is written flat while its row still records post_parent. The canonical path disagrees with the hierarchy the row declares, and a reader deriving the path from the parent chain cannot find the file.

Resolved through WP_Markdown_Storage::read_post(), which reads the post out of the corpus. get_post() would work under WordPress and fatal anywhere else — the smoke suite caught exactly that, since the runtime boots with no WordPress.

2. Residual predicates are absent from the provider projection

$provider_projection is assembled from the select list, scalar columns, scalar predicates, boolean predicates and subqueries — but not from $residual.

The post provider deliberately defers body predicates to the executor:

// Body predicates remain executor residuals until their candidates
// are hydrated; metadata predicates safely reduce sorting work here.

ordered_projection() then hydrates post_content only when it appears in the projection. For SELECT ID FROM wp_posts WHERE post_content LIKE '%x%' it does not, so the provider returns the lazily-resolved column empty and the executor matches the residual against ''.

Observed before the fix, on a row whose post_content is 5041 characters via get_post():

ID=8585 LIKE %Incident% -> 0 rows
ID=8585 LIKE %%%        -> 0 rows

Tests

tests/smoke-native-markdown-post-write.php gains a parent/child scenario: it asserts the child file lands in the parent directory and that the child's body is searchable at that path.

Verified the test discriminates. With the resolver change reverted and the test kept:

FAIL: a child post is written inside its parent directory

With both fixes: 7/7 in that file.

The two fixes are independent — with the resolver reverted, the body assertion still passes because the projection fix hydrates content at whatever path the file occupies.

Suite baseline

6 smoke files fail identically on main and on this branch, so this introduces no new failures: smoke-native-create-index, smoke-native-indexed-varchar-scan, smoke-native-option-query, smoke-native-plugin-schema-query, smoke-native-query-parser, smoke-native-table-replace.

Worth a separate issue: those files print FAIL: while exiting 0, so homeboy.json's php "$test_file" || exit 1 runner reports the suite green.

Not addressed here

This prevents new mismatches. Posts already written flat with a parent recorded still need a data repair, and rewriting them in place does not reliably relocate the file — that wants wp markdown-db reconcile rather than an wp_update_post() round trip.

AI assistance: GPT-6 Astra via OpenCode, under Chris Huber direction, traced the flat write and the empty body match to these two causes, made both changes, and wrote the regression. Chris Huber remains responsible for review.

The native runtime built its shared storage without a post resolver.
Every other backend wires one. resolve_parent_dir() needs it to walk a
post's ancestor chain, so without it the writer returns the post-type
root immediately and a child lands flat while its row still records the
parent. The canonical path then disagrees with the hierarchy the row
declares, and a reader that derives the path from the parent chain
cannot find the file.

Resolve through WP_Markdown_Storage::read_post(), which reads the post
out of the corpus. get_post() would work under WordPress and fatal
anywhere else, and the runtime has no WordPress to depend on.

A residual predicate is also matched in the executor after the provider
read, but the provider projection was built only from the select list,
the scalar and boolean predicates, and the subqueries. A provider that
resolves a column lazily returns it empty when it is absent from the
projection, so a body predicate matched against an empty string and
selected nothing. Even LIKE '%' returned no rows.

The regression writes a child under a parent, asserts the file lands in
the parent directory, and asserts the child's body is searchable there.
It fails on the prior resolver.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant