Skip to content

fix: wp_posts partitioned sync preserves markdown-type rows (closes #66)#67

Merged
chubes4 merged 1 commit into
mainfrom
fix/wp-posts-partition-sync
Apr 22, 2026
Merged

fix: wp_posts partitioned sync preserves markdown-type rows (closes #66)#67
chubes4 merged 1 commit into
mainfrom
fix/wp-posts-partition-sync

Conversation

@chubes4
Copy link
Copy Markdown
Collaborator

@chubes4 chubes4 commented Apr 21, 2026

Summary

Closes #66. Same partition hazard as #64/#65, now affecting wp_posts itself.

persist_non_markdown_posts() regenerates _tables/posts.json whenever any non-markdown post type is created or updated. Since WordPress auto-creates a revision on every wp_update_post() call, this fires on essentially every edit across the site — admin UI, Gutenberg, cron, CLI, REST.

sync_json_tables() then saw posts.json's mtime change on the next warm boot, truncated wp_posts, and reloaded the JSON — wiping every markdown-type post row until the following request's heal_orphaned_index_entries() could re-insert them from _markdown_file_index + disk.

That's a full request window during which wp_count_posts(), WP_Query, and every REST endpoint returned zero markdown-type posts. Briefings, scheduled flows, webhooks, and external API consumers all silently received empty data.

Live repro on intelligence-chubes4 (primary mode)

Before this PR:

$ sqlite3 markdown-index.sqlite "SELECT COUNT(*) FROM wp_posts WHERE post_type='wiki';"
141

$ wp eval 'wp_insert_post(["post_type"=>"revision","post_title"=>"x","post_status"=>"inherit"]);'

$ wp eval "echo wp_count_posts('wiki')->publish;"
0                # entire wiki invisible

$ sqlite3 markdown-index.sqlite "SELECT COUNT(*) FROM wp_posts WHERE post_type='wiki';"
0                # every wiki row wiped

$ wp eval "echo wp_count_posts('wiki')->publish;"
141              # heal recovered on the next boot

After this PR:

$ wp eval 'wp_insert_post(["post_type"=>"revision","post_title"=>"x","post_status"=>"inherit"]);'

$ wp eval "echo wp_count_posts('wiki')->publish;"
135              # published rows intact

$ sqlite3 markdown-index.sqlite "SELECT COUNT(*) FROM wp_posts WHERE post_type='wiki';"
141              # all rows preserved, including drafts

Fix

New private helper sync_posts_partition_from_json() runs a surgical
DELETE FROM wp_posts WHERE post_type IN (excluded_types) before the JSON reload, leaving markdown-type rows untouched. sync_json_tables() dispatches to it when the changed file is posts.json.

The IN-clause quoting path is extracted into quote_excluded_types_for_in_clause() and shared between sync_partitioned_table_from_json() (#65) and the new sync_posts_partition_from_json().

Why this wasn't caught by #64/#65's fix

The #65 helper filters rows by looking up post_id in wp_posts.post_type — that works for postmeta/term_relationships where the post_type is one join away. For wp_posts itself the filter is on wp_posts.post_type directly, so it needs its own helper. Otherwise the shape is identical.

Tests

  • tests/smoke-sync-posts-partition.php — 22 new pure-PHP smokes:
    • markdown-type rows survive a partitioned sync (wiki + post + page all preserved)
    • empty excluded-types list is a no-op
    • exact (non-prefix) post_type matching — revision_draft is NOT deleted when revision is excluded
    • SQL-injection-safe quoting of hostile post type names
    • quote_excluded_types_for_in_clause() helper shape including SQL-standard '' escape for embedded single quotes
    • End-to-end issue sync_json_tables wipes wp_posts rows for markdown-type posts when posts.json changes #66 repro: 141 wiki rows survive warm-boot sync after posts.json rewrite
  • Existing smokes all still pass (smoke-inject-id.php — 23 ✓, smoke-search.php — 20 ✓, smoke-sync-partitioned.php — 12 ✓).

Remaining work

Bug B (parent-promotion _markdown_file_index staleness) is independent and will follow in a separate PR.

Same partition hazard as #64/#65, now affecting wp_posts itself.

persist_non_markdown_posts() regenerates _tables/posts.json whenever any
non-markdown post type is created or updated. WordPress auto-creates a
revision on every wp_update_post() call, so this fires on essentially
every edit across the site.

When sync_json_tables() saw posts.json's mtime change on the next warm
boot, it truncated wp_posts and reloaded from JSON — wiping every
markdown-type post row until the following request's heal_orphaned_
index_entries() could re-insert them from _markdown_file_index + disk.
A full request window during which wp_count_posts(), WP_Query, and
every REST endpoint returned zero markdown-type posts.

Fix: add sync_posts_partition_from_json() that deletes only rows whose
post_type is in the excluded (non-markdown) list, then reloads JSON.
Markdown-type rows sourced from frontmatter survive the sync.

Extracts the IN-clause quoting into quote_excluded_types_for_in_clause()
so sync_partitioned_table_from_json() and sync_posts_partition_from_json()
share the escaping path.

Adds 22 pure-PHP smoke tests in tests/smoke-sync-posts-partition.php
covering the happy path, empty excluded-types list, exact (non-prefix)
post_type matching, SQL-injection-safe quoting, helper shape, and the
live repro from issue #66 (141 wiki rows survive warm boot after a
posts.json rewrite).
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.

sync_json_tables wipes wp_posts rows for markdown-type posts when posts.json changes

1 participant