From 15bfff4a34fc5804457b968bc52f7ec99eaa8201 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 29 Aug 2026 11:52:24 -0400 Subject: [PATCH 1/9] test: benchmark production query shapes --- rigs/mdi-native/rig.json | 10 ++- rigs/mdi-sqlite/rig.json | 10 ++- tests/bench/README.md | 2 + tests/bench/plugin-table-inventory.php | 96 +++++++++++++++++++++++ tests/bench/wiki-hierarchy.php | 101 +++++++++++++++++++++++++ 5 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 tests/bench/plugin-table-inventory.php create mode 100644 tests/bench/wiki-hierarchy.php diff --git a/rigs/mdi-native/rig.json b/rigs/mdi-native/rig.json index dbb5b52..7a94999 100644 --- a/rigs/mdi-native/rig.json +++ b/rigs/mdi-native/rig.json @@ -52,6 +52,12 @@ }, { "path": "${package.root}/tests/bench/read-heavy.php" + }, + { + "path": "${package.root}/tests/bench/wiki-hierarchy.php" + }, + { + "path": "${package.root}/tests/bench/plugin-table-inventory.php" } ] }, @@ -60,7 +66,9 @@ "boot-timing", "bulk-import", "obsidian-bursty", - "read-heavy" + "read-heavy", + "wiki-hierarchy", + "plugin-table-inventory" ] }, "pipeline": { diff --git a/rigs/mdi-sqlite/rig.json b/rigs/mdi-sqlite/rig.json index 63a455b..a88216a 100644 --- a/rigs/mdi-sqlite/rig.json +++ b/rigs/mdi-sqlite/rig.json @@ -49,6 +49,12 @@ }, { "path": "${package.root}/tests/bench/read-heavy.php" + }, + { + "path": "${package.root}/tests/bench/wiki-hierarchy.php" + }, + { + "path": "${package.root}/tests/bench/plugin-table-inventory.php" } ] }, @@ -57,7 +63,9 @@ "boot-timing", "bulk-import", "obsidian-bursty", - "read-heavy" + "read-heavy", + "wiki-hierarchy", + "plugin-table-inventory" ] }, "pipeline": { diff --git a/tests/bench/README.md b/tests/bench/README.md index c9c35d0..8f01316 100644 --- a/tests/bench/README.md +++ b/tests/bench/README.md @@ -99,6 +99,8 @@ tests/bench/ ├── crash-kill.php ← workload: simulated mid-write interrupt, shared-state-required ├── obsidian-bursty.php ← workload: 70%U / 20%C / 5%R / 3%P / 2%D against persistent corpus ├── read-heavy.php ← workload: get_post / by_slug / date / tax / search mix +├── wiki-hierarchy.php ← workload: ordered hierarchy scan with postmeta exclusions +├── plugin-table-inventory.php ← workload: dynamic plugin-table scans and lifecycle upserts └── results/ ← gitignored output dir, .gitkeep retained tests/bench-lib/ diff --git a/tests/bench/plugin-table-inventory.php b/tests/bench/plugin-table-inventory.php new file mode 100644 index 0000000..3add496 --- /dev/null +++ b/tests/bench/plugin-table-inventory.php @@ -0,0 +1,96 @@ +prefix . 'bench_worktree_inventory'; + $wpdb->query( + "CREATE TABLE IF NOT EXISTS {$table} ( + id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + handle varchar(191) NOT NULL, + repo varchar(191) NOT NULL DEFAULT '', + lifecycle_state varchar(64) DEFAULT NULL, + task_url text DEFAULT NULL, + owner_run_ref varchar(191) DEFAULT NULL, + missing_path tinyint(1) NOT NULL DEFAULT 0, + metadata longtext DEFAULT NULL, + updated_at datetime NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY handle (handle), + KEY repo (repo), + KEY lifecycle_state (lifecycle_state), + KEY missing_path (missing_path) + )" + ); + + if (!$seeded) { + $existing = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); + for ($i = $existing; $i < 500; $i++) { + $wpdb->insert($table, [ + 'handle' => sprintf('repo-%02d@branch-%04d', $i % 25, $i), + 'repo' => sprintf('repo-%02d', $i % 25), + 'lifecycle_state' => 0 === $i % 3 ? 'cleanup_eligible' : 'active', + 'task_url' => 'https://example.com/issues/' . ($i % 100), + 'owner_run_ref' => 'bench-run-' . $i, + 'missing_path' => 0 === $i % 10 ? 1 : 0, + 'metadata' => wp_json_encode(['sequence' => $i, 'source' => 'benchmark']), + 'updated_at' => '2026-08-29 00:00:00', + ]); + } + $seeded = true; + } + + $all_rows = $wpdb->get_results("SELECT * FROM {$table} ORDER BY handle ASC", ARRAY_A); + $repo_rows = $wpdb->get_results( + $wpdb->prepare("SELECT * FROM {$table} WHERE repo = %s ORDER BY handle ASC", 'repo-07'), + ARRAY_A + ); + $task_rows = $wpdb->get_results( + $wpdb->prepare( + "SELECT * FROM {$table} WHERE task_url = %s OR LOWER(owner_run_ref) = LOWER(%s) ORDER BY handle ASC LIMIT %d", + 'https://example.com/issues/42', + 'bench-run-42', + 201 + ), + ARRAY_A + ); + + $sequence = count($all_rows); + $replaced = $wpdb->replace($table, [ + 'handle' => 'repo-00@benchmark-current', + 'repo' => 'repo-00', + 'lifecycle_state' => 'active', + 'task_url' => 'https://example.com/issues/current', + 'owner_run_ref' => 'bench-current-' . $sequence, + 'missing_path' => 0, + 'metadata' => wp_json_encode(['sequence' => $sequence, 'source' => 'benchmark']), + 'updated_at' => '2026-08-29 00:00:00', + ]); + + if (!is_array($all_rows) || !is_array($repo_rows) || !is_array($task_rows) || false === $replaced || '' !== (string) $wpdb->last_error) { + throw new RuntimeException('Plugin-table inventory workload failed: ' . (string) $wpdb->last_error); + } + + return [ + 'metrics' => [ + 'inventory_rows' => count($all_rows), + 'repo_rows' => count($repo_rows), + 'task_rows' => count($task_rows), + 'replace_result' => (int) $replaced, + ], + 'metadata' => [ + 'query_shape' => 'dynamic plugin table ordered scans, filtered lookup, and REPLACE upsert', + ], + ]; +}; diff --git a/tests/bench/wiki-hierarchy.php b/tests/bench/wiki-hierarchy.php new file mode 100644 index 0000000..ca452d7 --- /dev/null +++ b/tests/bench/wiki-hierarchy.php @@ -0,0 +1,101 @@ +get_var( + $wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_name = %s LIMIT 1", + 'wiki', + $root_slug + ) + ); + + if (!$seeded && $root_id <= 0) { + $root_id = (int) wp_insert_post([ + 'post_type' => 'wiki', + 'post_status' => 'publish', + 'post_title' => 'Bench Wiki Hierarchy', + 'post_name' => $root_slug, + ]); + + for ($topic = 0; $topic < 20; $topic++) { + $topic_id = (int) wp_insert_post([ + 'post_type' => 'wiki', + 'post_status' => 'publish', + 'post_parent' => $root_id, + 'post_title' => sprintf('Bench Wiki Topic %02d', $topic), + 'post_name' => sprintf('bench-wiki-topic-%02d', $topic), + 'menu_order' => $topic, + ]); + + for ($article = 0; $article < 49; $article++) { + $article_id = (int) wp_insert_post([ + 'post_type' => 'wiki', + 'post_status' => 'publish', + 'post_parent' => $topic_id, + 'post_title' => sprintf('Bench Wiki Topic %02d Article %02d', $topic, $article), + 'post_name' => sprintf('bench-wiki-topic-%02d-article-%02d', $topic, $article), + 'post_content' => 'Synthetic wiki article for hierarchy query benchmarking.', + 'menu_order' => $article, + ]); + + if (0 === $article % 20) { + add_post_meta($article_id, '_intelligence_wiki_observation', '1', true); + } elseif (1 === $article % 20) { + add_post_meta($article_id, '_intelligence_wiki_calendar_parent', '1', true); + } + } + } + } + $seeded = true; + + $rows = $wpdb->get_results( + $wpdb->prepare( + "SELECT p.ID, p.post_parent, p.post_name, p.post_title + FROM {$wpdb->posts} p + WHERE p.post_type = %s + AND p.post_status = %s + AND NOT EXISTS ( + SELECT 1 FROM {$wpdb->postmeta} observation_meta + WHERE observation_meta.post_id = p.ID AND observation_meta.meta_key = %s + ) + AND NOT EXISTS ( + SELECT 1 FROM {$wpdb->postmeta} calendar_meta + WHERE calendar_meta.post_id = p.ID AND calendar_meta.meta_key = %s + ) + ORDER BY p.menu_order ASC, p.post_title ASC", + 'wiki', + 'publish', + '_intelligence_wiki_observation', + '_intelligence_wiki_calendar_parent' + ) + ); + + if (!is_array($rows) || '' !== (string) $wpdb->last_error) { + throw new RuntimeException('Wiki hierarchy query failed: ' . (string) $wpdb->last_error); + } + + return [ + 'metrics' => [ + 'rows_returned' => count($rows), + 'seeded_posts' => 1001, + 'excluded_rows' => 100, + ], + 'metadata' => [ + 'query_shape' => 'posts hierarchy ordered with two correlated postmeta NOT EXISTS predicates', + ], + ]; +}; From ccca97f4a9576bc0e2b1e4408cdbb02a3a13627e Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 29 Aug 2026 12:52:30 -0400 Subject: [PATCH 2/9] test: attest benchmark database backend --- tests/bench-lib/shared-helpers.php | 29 ++++++++++++++++++++++++++ tests/bench/boot-timing.php | 2 ++ tests/bench/bulk-import.php | 2 ++ tests/bench/obsidian-bursty.php | 2 ++ tests/bench/plugin-table-inventory.php | 5 +++++ tests/bench/read-heavy.php | 2 ++ tests/bench/wiki-hierarchy.php | 5 +++++ 7 files changed, 47 insertions(+) diff --git a/tests/bench-lib/shared-helpers.php b/tests/bench-lib/shared-helpers.php index dccab7e..3ddd903 100644 --- a/tests/bench-lib/shared-helpers.php +++ b/tests/bench-lib/shared-helpers.php @@ -68,6 +68,35 @@ function mdi_bench_corpus_size(): int { return $n > 0 ? $n : MDI_BENCH_DEFAULT_CORPUS_SIZE; } +/** + * Verify that the configured database backend owns the active wpdb boundary. + * + * @return array{backend:string,wpdb_class:string} + */ +function mdi_bench_runtime(): array { + global $wpdb; + + $backend = defined('MARKDOWN_DB_BACKEND') ? (string) MARKDOWN_DB_BACKEND : 'sqlite'; + $wpdb_class = is_object($wpdb) ? get_class($wpdb) : gettype($wpdb); + $native_active = class_exists('WP_Markdown_Native_WPDB', false) && $wpdb instanceof WP_Markdown_Native_WPDB; + + if ('mdi-native' === $backend && !$native_active) { + throw new RuntimeException( + sprintf('Configured mdi-native benchmark booted %s instead of WP_Markdown_Native_WPDB.', $wpdb_class) + ); + } + if ('mdi-native' !== $backend && $native_active) { + throw new RuntimeException( + sprintf('Configured %s benchmark unexpectedly booted WP_Markdown_Native_WPDB.', $backend) + ); + } + + return [ + 'backend' => $backend, + 'wpdb_class' => $wpdb_class, + ]; +} + /** * Seed mt_rand for deterministic corpus + workload streams. * diff --git a/tests/bench/boot-timing.php b/tests/bench/boot-timing.php index 7d5b748..e579bd4 100644 --- a/tests/bench/boot-timing.php +++ b/tests/bench/boot-timing.php @@ -14,6 +14,8 @@ return function (): array { static $iteration = 0; + mdi_bench_runtime(); + $phase = getenv('BENCH_BOOT_PHASE'); if ($phase === false || trim($phase) === '') { return [ diff --git a/tests/bench/bulk-import.php b/tests/bench/bulk-import.php index 58c86f3..ed1ef6b 100644 --- a/tests/bench/bulk-import.php +++ b/tests/bench/bulk-import.php @@ -40,6 +40,8 @@ require_once __DIR__ . '/../bench-lib/shared-helpers.php'; return function (): array { + mdi_bench_runtime(); + $size = mdi_bench_corpus_size(); // Reset the posts table so iteration-K starts with the same blank diff --git a/tests/bench/obsidian-bursty.php b/tests/bench/obsidian-bursty.php index 3c57c7f..77d12a0 100644 --- a/tests/bench/obsidian-bursty.php +++ b/tests/bench/obsidian-bursty.php @@ -41,6 +41,8 @@ static $next_create = 0; static $ops_per_iter = 50; // 50 mixed ops per dispatcher iteration + mdi_bench_runtime(); + if ($live_ids === null) { // First call — seed the corpus. mdi_bench_seed(); diff --git a/tests/bench/plugin-table-inventory.php b/tests/bench/plugin-table-inventory.php index 3add496..97b8cd0 100644 --- a/tests/bench/plugin-table-inventory.php +++ b/tests/bench/plugin-table-inventory.php @@ -9,10 +9,13 @@ * @package Markdown_Database_Integration\Tests\Bench */ +require_once __DIR__ . '/../bench-lib/shared-helpers.php'; + return function (): array { static $seeded = false; global $wpdb; + $runtime = mdi_bench_runtime(); $table = $wpdb->prefix . 'bench_worktree_inventory'; $wpdb->query( @@ -91,6 +94,8 @@ ], 'metadata' => [ 'query_shape' => 'dynamic plugin table ordered scans, filtered lookup, and REPLACE upsert', + 'backend' => $runtime['backend'], + 'wpdb_class' => $runtime['wpdb_class'], ], ]; }; diff --git a/tests/bench/read-heavy.php b/tests/bench/read-heavy.php index 29431de..4a938ab 100644 --- a/tests/bench/read-heavy.php +++ b/tests/bench/read-heavy.php @@ -41,6 +41,8 @@ static $tag_term_ids = null; static $ops_per_iter = 100; + mdi_bench_runtime(); + if ($ids === null) { mdi_bench_seed(); $ids = mdi_bench_seed_corpus(mdi_bench_corpus_size()); diff --git a/tests/bench/wiki-hierarchy.php b/tests/bench/wiki-hierarchy.php index ca452d7..2e2c890 100644 --- a/tests/bench/wiki-hierarchy.php +++ b/tests/bench/wiki-hierarchy.php @@ -9,10 +9,13 @@ * @package Markdown_Database_Integration\Tests\Bench */ +require_once __DIR__ . '/../bench-lib/shared-helpers.php'; + return function (): array { static $seeded = false; global $wpdb; + $runtime = mdi_bench_runtime(); $root_slug = 'bench-wiki-hierarchy'; $root_id = (int) $wpdb->get_var( @@ -96,6 +99,8 @@ ], 'metadata' => [ 'query_shape' => 'posts hierarchy ordered with two correlated postmeta NOT EXISTS predicates', + 'backend' => $runtime['backend'], + 'wpdb_class' => $runtime['wpdb_class'], ], ]; }; From a1ecea471234570bd8300bd87dc1f934577063f6 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 29 Aug 2026 13:30:05 -0400 Subject: [PATCH 3/9] test: seed native benchmark runtime --- rigs/mdi-native/rig.json | 22 +++++++++++++++++-- .../_options/active_plugins.json | 6 +++++ .../_options/admin_email.json | 6 +++++ .../_options/blogdescription.json | 6 +++++ .../native-bench-state/_options/blogname.json | 6 +++++ .../_options/comments_notify.json | 6 +++++ .../_options/date_format.json | 6 +++++ .../_options/db_version.json | 6 +++++ .../_options/default_category.json | 6 +++++ .../_options/default_comment_status.json | 6 +++++ .../_options/default_ping_status.json | 6 +++++ .../_options/default_pingback_flag.json | 6 +++++ .../_options/fresh_site.json | 6 +++++ .../_options/gmt_offset.json | 6 +++++ .../native-bench-state/_options/home.json | 6 +++++ .../_options/initial_db_version.json | 6 +++++ .../_options/links_updated_date_format.json | 6 +++++ .../_options/mailserver_login.json | 6 +++++ .../_options/mailserver_pass.json | 6 +++++ .../_options/mailserver_port.json | 6 +++++ .../_options/mailserver_url.json | 6 +++++ .../_options/posts_per_page.json | 6 +++++ .../_options/posts_per_rss.json | 6 +++++ .../_options/require_name_email.json | 6 +++++ .../_options/rss_use_excerpt.json | 6 +++++ .../native-bench-state/_options/siteurl.json | 6 +++++ .../_options/start_of_week.json | 6 +++++ .../_options/stylesheet.json | 6 +++++ .../native-bench-state/_options/template.json | 6 +++++ .../_options/time_format.json | 6 +++++ .../_options/timezone_string.json | 6 +++++ .../_options/use_balanceTags.json | 6 +++++ .../_options/use_smilies.json | 6 +++++ .../_options/users_can_register.json | 6 +++++ .../_options/wp_user_roles.json | 6 +++++ .../native-bench-state/_tables/usermeta.json | 14 ++++++++++++ .../native-bench-state/_tables/users.json | 14 ++++++++++++ tests/generate-native-bench-state.php | 19 ++++++++++++++++ 38 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/native-bench-state/_options/active_plugins.json create mode 100644 tests/fixtures/native-bench-state/_options/admin_email.json create mode 100644 tests/fixtures/native-bench-state/_options/blogdescription.json create mode 100644 tests/fixtures/native-bench-state/_options/blogname.json create mode 100644 tests/fixtures/native-bench-state/_options/comments_notify.json create mode 100644 tests/fixtures/native-bench-state/_options/date_format.json create mode 100644 tests/fixtures/native-bench-state/_options/db_version.json create mode 100644 tests/fixtures/native-bench-state/_options/default_category.json create mode 100644 tests/fixtures/native-bench-state/_options/default_comment_status.json create mode 100644 tests/fixtures/native-bench-state/_options/default_ping_status.json create mode 100644 tests/fixtures/native-bench-state/_options/default_pingback_flag.json create mode 100644 tests/fixtures/native-bench-state/_options/fresh_site.json create mode 100644 tests/fixtures/native-bench-state/_options/gmt_offset.json create mode 100644 tests/fixtures/native-bench-state/_options/home.json create mode 100644 tests/fixtures/native-bench-state/_options/initial_db_version.json create mode 100644 tests/fixtures/native-bench-state/_options/links_updated_date_format.json create mode 100644 tests/fixtures/native-bench-state/_options/mailserver_login.json create mode 100644 tests/fixtures/native-bench-state/_options/mailserver_pass.json create mode 100644 tests/fixtures/native-bench-state/_options/mailserver_port.json create mode 100644 tests/fixtures/native-bench-state/_options/mailserver_url.json create mode 100644 tests/fixtures/native-bench-state/_options/posts_per_page.json create mode 100644 tests/fixtures/native-bench-state/_options/posts_per_rss.json create mode 100644 tests/fixtures/native-bench-state/_options/require_name_email.json create mode 100644 tests/fixtures/native-bench-state/_options/rss_use_excerpt.json create mode 100644 tests/fixtures/native-bench-state/_options/siteurl.json create mode 100644 tests/fixtures/native-bench-state/_options/start_of_week.json create mode 100644 tests/fixtures/native-bench-state/_options/stylesheet.json create mode 100644 tests/fixtures/native-bench-state/_options/template.json create mode 100644 tests/fixtures/native-bench-state/_options/time_format.json create mode 100644 tests/fixtures/native-bench-state/_options/timezone_string.json create mode 100644 tests/fixtures/native-bench-state/_options/use_balanceTags.json create mode 100644 tests/fixtures/native-bench-state/_options/use_smilies.json create mode 100644 tests/fixtures/native-bench-state/_options/users_can_register.json create mode 100644 tests/fixtures/native-bench-state/_options/wp_user_roles.json create mode 100644 tests/fixtures/native-bench-state/_tables/usermeta.json create mode 100644 tests/fixtures/native-bench-state/_tables/users.json create mode 100644 tests/generate-native-bench-state.php diff --git a/rigs/mdi-native/rig.json b/rigs/mdi-native/rig.json index 7a94999..2a5a1dd 100644 --- a/rigs/mdi-native/rig.json +++ b/rigs/mdi-native/rig.json @@ -21,8 +21,26 @@ "BENCH_CORPUS_SIZE": "100" }, "wp_config_defines": { - "MARKDOWN_DB_BACKEND": "mdi-native" - } + "MARKDOWN_DB_BACKEND": "mdi-native", + "MARKDOWN_DB_CONTENT_DIR": "/wordpress/wp-content/db", + "MARKDOWN_DB_STATE_DIR": "/wordpress/wp-content/db" + }, + "wp_codebox_bench_mounts": [ + { + "source": "${components.markdown-database-integration.path}/db.php", + "target": "/wordpress/wp-content/db.php", + "type": "file", + "mode": "readonly", + "phase": "pre-install" + }, + { + "source": "${components.markdown-database-integration.path}/tests/fixtures/native-bench-state", + "target": "/wordpress/wp-content/db", + "type": "directory", + "mode": "readonly", + "phase": "pre-install" + } + ] } } } diff --git a/tests/fixtures/native-bench-state/_options/active_plugins.json b/tests/fixtures/native-bench-state/_options/active_plugins.json new file mode 100644 index 0000000..3623c6a --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/active_plugins.json @@ -0,0 +1,6 @@ +{ + "option_id": 28, + "option_name": "active_plugins", + "option_value": "a:0:{}", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/admin_email.json b/tests/fixtures/native-bench-state/_options/admin_email.json new file mode 100644 index 0000000..d3beab4 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/admin_email.json @@ -0,0 +1,6 @@ +{ + "option_id": 6, + "option_name": "admin_email", + "option_value": "admin@example.test", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/blogdescription.json b/tests/fixtures/native-bench-state/_options/blogdescription.json new file mode 100644 index 0000000..1a5ae98 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/blogdescription.json @@ -0,0 +1,6 @@ +{ + "option_id": 4, + "option_name": "blogdescription", + "option_value": "", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/blogname.json b/tests/fixtures/native-bench-state/_options/blogname.json new file mode 100644 index 0000000..9add865 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/blogname.json @@ -0,0 +1,6 @@ +{ + "option_id": 3, + "option_name": "blogname", + "option_value": "MDI Native Lifecycle", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/comments_notify.json b/tests/fixtures/native-bench-state/_options/comments_notify.json new file mode 100644 index 0000000..342ce89 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/comments_notify.json @@ -0,0 +1,6 @@ +{ + "option_id": 11, + "option_name": "comments_notify", + "option_value": "1", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/date_format.json b/tests/fixtures/native-bench-state/_options/date_format.json new file mode 100644 index 0000000..0ba1eca --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/date_format.json @@ -0,0 +1,6 @@ +{ + "option_id": 23, + "option_name": "date_format", + "option_value": "F j, Y", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/db_version.json b/tests/fixtures/native-bench-state/_options/db_version.json new file mode 100644 index 0000000..18b14f9 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/db_version.json @@ -0,0 +1,6 @@ +{ + "option_id": 29, + "option_name": "db_version", + "option_value": "61833", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/default_category.json b/tests/fixtures/native-bench-state/_options/default_category.json new file mode 100644 index 0000000..1fdc189 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/default_category.json @@ -0,0 +1,6 @@ +{ + "option_id": 18, + "option_name": "default_category", + "option_value": "1", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/default_comment_status.json b/tests/fixtures/native-bench-state/_options/default_comment_status.json new file mode 100644 index 0000000..56862ae --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/default_comment_status.json @@ -0,0 +1,6 @@ +{ + "option_id": 19, + "option_name": "default_comment_status", + "option_value": "open", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/default_ping_status.json b/tests/fixtures/native-bench-state/_options/default_ping_status.json new file mode 100644 index 0000000..dbb09c8 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/default_ping_status.json @@ -0,0 +1,6 @@ +{ + "option_id": 20, + "option_name": "default_ping_status", + "option_value": "open", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/default_pingback_flag.json b/tests/fixtures/native-bench-state/_options/default_pingback_flag.json new file mode 100644 index 0000000..8590c46 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/default_pingback_flag.json @@ -0,0 +1,6 @@ +{ + "option_id": 21, + "option_name": "default_pingback_flag", + "option_value": "1", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/fresh_site.json b/tests/fixtures/native-bench-state/_options/fresh_site.json new file mode 100644 index 0000000..791c187 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/fresh_site.json @@ -0,0 +1,6 @@ +{ + "option_id": 31, + "option_name": "fresh_site", + "option_value": "1", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/gmt_offset.json b/tests/fixtures/native-bench-state/_options/gmt_offset.json new file mode 100644 index 0000000..5b08f01 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/gmt_offset.json @@ -0,0 +1,6 @@ +{ + "option_id": 27, + "option_name": "gmt_offset", + "option_value": "0", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/home.json b/tests/fixtures/native-bench-state/_options/home.json new file mode 100644 index 0000000..d3f5e16 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/home.json @@ -0,0 +1,6 @@ +{ + "option_id": 2, + "option_name": "home", + "option_value": "http://localhost", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/initial_db_version.json b/tests/fixtures/native-bench-state/_options/initial_db_version.json new file mode 100644 index 0000000..91c0e55 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/initial_db_version.json @@ -0,0 +1,6 @@ +{ + "option_id": 30, + "option_name": "initial_db_version", + "option_value": "61833", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/links_updated_date_format.json b/tests/fixtures/native-bench-state/_options/links_updated_date_format.json new file mode 100644 index 0000000..2f6e228 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/links_updated_date_format.json @@ -0,0 +1,6 @@ +{ + "option_id": 25, + "option_name": "links_updated_date_format", + "option_value": "F j, Y g:i a", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/mailserver_login.json b/tests/fixtures/native-bench-state/_options/mailserver_login.json new file mode 100644 index 0000000..7460e69 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/mailserver_login.json @@ -0,0 +1,6 @@ +{ + "option_id": 15, + "option_name": "mailserver_login", + "option_value": "login@example.test", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/mailserver_pass.json b/tests/fixtures/native-bench-state/_options/mailserver_pass.json new file mode 100644 index 0000000..21071b9 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/mailserver_pass.json @@ -0,0 +1,6 @@ +{ + "option_id": 16, + "option_name": "mailserver_pass", + "option_value": "password", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/mailserver_port.json b/tests/fixtures/native-bench-state/_options/mailserver_port.json new file mode 100644 index 0000000..a055149 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/mailserver_port.json @@ -0,0 +1,6 @@ +{ + "option_id": 17, + "option_name": "mailserver_port", + "option_value": "110", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/mailserver_url.json b/tests/fixtures/native-bench-state/_options/mailserver_url.json new file mode 100644 index 0000000..a6f95ac --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/mailserver_url.json @@ -0,0 +1,6 @@ +{ + "option_id": 14, + "option_name": "mailserver_url", + "option_value": "mail.example.test", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/posts_per_page.json b/tests/fixtures/native-bench-state/_options/posts_per_page.json new file mode 100644 index 0000000..cb870a0 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/posts_per_page.json @@ -0,0 +1,6 @@ +{ + "option_id": 22, + "option_name": "posts_per_page", + "option_value": "10", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/posts_per_rss.json b/tests/fixtures/native-bench-state/_options/posts_per_rss.json new file mode 100644 index 0000000..3171702 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/posts_per_rss.json @@ -0,0 +1,6 @@ +{ + "option_id": 12, + "option_name": "posts_per_rss", + "option_value": "10", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/require_name_email.json b/tests/fixtures/native-bench-state/_options/require_name_email.json new file mode 100644 index 0000000..b96eb4d --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/require_name_email.json @@ -0,0 +1,6 @@ +{ + "option_id": 10, + "option_name": "require_name_email", + "option_value": "1", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/rss_use_excerpt.json b/tests/fixtures/native-bench-state/_options/rss_use_excerpt.json new file mode 100644 index 0000000..82edace --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/rss_use_excerpt.json @@ -0,0 +1,6 @@ +{ + "option_id": 13, + "option_name": "rss_use_excerpt", + "option_value": "0", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/siteurl.json b/tests/fixtures/native-bench-state/_options/siteurl.json new file mode 100644 index 0000000..d6dff1a --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/siteurl.json @@ -0,0 +1,6 @@ +{ + "option_id": 1, + "option_name": "siteurl", + "option_value": "http://localhost", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/start_of_week.json b/tests/fixtures/native-bench-state/_options/start_of_week.json new file mode 100644 index 0000000..d253fad --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/start_of_week.json @@ -0,0 +1,6 @@ +{ + "option_id": 7, + "option_name": "start_of_week", + "option_value": "1", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/stylesheet.json b/tests/fixtures/native-bench-state/_options/stylesheet.json new file mode 100644 index 0000000..474f925 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/stylesheet.json @@ -0,0 +1,6 @@ +{ + "option_id": 33, + "option_name": "stylesheet", + "option_value": "twentytwentyfive", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/template.json b/tests/fixtures/native-bench-state/_options/template.json new file mode 100644 index 0000000..caabcb5 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/template.json @@ -0,0 +1,6 @@ +{ + "option_id": 32, + "option_name": "template", + "option_value": "twentytwentyfive", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/time_format.json b/tests/fixtures/native-bench-state/_options/time_format.json new file mode 100644 index 0000000..d42842f --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/time_format.json @@ -0,0 +1,6 @@ +{ + "option_id": 24, + "option_name": "time_format", + "option_value": "g:i a", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/timezone_string.json b/tests/fixtures/native-bench-state/_options/timezone_string.json new file mode 100644 index 0000000..e38d932 --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/timezone_string.json @@ -0,0 +1,6 @@ +{ + "option_id": 26, + "option_name": "timezone_string", + "option_value": "", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/use_balanceTags.json b/tests/fixtures/native-bench-state/_options/use_balanceTags.json new file mode 100644 index 0000000..42c744f --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/use_balanceTags.json @@ -0,0 +1,6 @@ +{ + "option_id": 8, + "option_name": "use_balanceTags", + "option_value": "0", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/use_smilies.json b/tests/fixtures/native-bench-state/_options/use_smilies.json new file mode 100644 index 0000000..8fe5fba --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/use_smilies.json @@ -0,0 +1,6 @@ +{ + "option_id": 9, + "option_name": "use_smilies", + "option_value": "1", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/users_can_register.json b/tests/fixtures/native-bench-state/_options/users_can_register.json new file mode 100644 index 0000000..e813dcf --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/users_can_register.json @@ -0,0 +1,6 @@ +{ + "option_id": 5, + "option_name": "users_can_register", + "option_value": "0", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_options/wp_user_roles.json b/tests/fixtures/native-bench-state/_options/wp_user_roles.json new file mode 100644 index 0000000..194170b --- /dev/null +++ b/tests/fixtures/native-bench-state/_options/wp_user_roles.json @@ -0,0 +1,6 @@ +{ + "option_id": 34, + "option_name": "wp_user_roles", + "option_value": "a:5:{s:13:\"administrator\";a:2:{s:4:\"name\";s:13:\"Administrator\";s:12:\"capabilities\";a:61:{s:13:\"switch_themes\";b:1;s:11:\"edit_themes\";b:1;s:16:\"activate_plugins\";b:1;s:12:\"edit_plugins\";b:1;s:10:\"edit_users\";b:1;s:10:\"edit_files\";b:1;s:14:\"manage_options\";b:1;s:17:\"moderate_comments\";b:1;s:17:\"manage_categories\";b:1;s:12:\"manage_links\";b:1;s:12:\"upload_files\";b:1;s:6:\"import\";b:1;s:15:\"unfiltered_html\";b:1;s:10:\"edit_posts\";b:1;s:17:\"edit_others_posts\";b:1;s:20:\"edit_published_posts\";b:1;s:13:\"publish_posts\";b:1;s:10:\"edit_pages\";b:1;s:4:\"read\";b:1;s:8:\"level_10\";b:1;s:7:\"level_9\";b:1;s:7:\"level_8\";b:1;s:7:\"level_7\";b:1;s:7:\"level_6\";b:1;s:7:\"level_5\";b:1;s:7:\"level_4\";b:1;s:7:\"level_3\";b:1;s:7:\"level_2\";b:1;s:7:\"level_1\";b:1;s:7:\"level_0\";b:1;s:17:\"edit_others_pages\";b:1;s:20:\"edit_published_pages\";b:1;s:13:\"publish_pages\";b:1;s:12:\"delete_pages\";b:1;s:19:\"delete_others_pages\";b:1;s:22:\"delete_published_pages\";b:1;s:12:\"delete_posts\";b:1;s:19:\"delete_others_posts\";b:1;s:22:\"delete_published_posts\";b:1;s:20:\"delete_private_posts\";b:1;s:18:\"edit_private_posts\";b:1;s:18:\"read_private_posts\";b:1;s:20:\"delete_private_pages\";b:1;s:18:\"edit_private_pages\";b:1;s:18:\"read_private_pages\";b:1;s:12:\"delete_users\";b:1;s:12:\"create_users\";b:1;s:17:\"unfiltered_upload\";b:1;s:14:\"edit_dashboard\";b:1;s:14:\"update_plugins\";b:1;s:14:\"delete_plugins\";b:1;s:15:\"install_plugins\";b:1;s:13:\"update_themes\";b:1;s:14:\"install_themes\";b:1;s:11:\"update_core\";b:1;s:10:\"list_users\";b:1;s:12:\"remove_users\";b:1;s:13:\"promote_users\";b:1;s:18:\"edit_theme_options\";b:1;s:13:\"delete_themes\";b:1;s:6:\"export\";b:1;}}s:6:\"editor\";a:2:{s:4:\"name\";s:6:\"Editor\";s:12:\"capabilities\";a:5:{s:4:\"read\";b:1;s:10:\"edit_posts\";b:1;s:17:\"edit_others_posts\";b:1;s:13:\"publish_posts\";b:1;s:12:\"upload_files\";b:1;}}s:6:\"author\";a:2:{s:4:\"name\";s:6:\"Author\";s:12:\"capabilities\";a:4:{s:4:\"read\";b:1;s:10:\"edit_posts\";b:1;s:13:\"publish_posts\";b:1;s:12:\"upload_files\";b:1;}}s:11:\"contributor\";a:2:{s:4:\"name\";s:11:\"Contributor\";s:12:\"capabilities\";a:2:{s:4:\"read\";b:1;s:10:\"edit_posts\";b:1;}}s:10:\"subscriber\";a:2:{s:4:\"name\";s:10:\"Subscriber\";s:12:\"capabilities\";a:1:{s:4:\"read\";b:1;}}}", + "autoload": "on" +} diff --git a/tests/fixtures/native-bench-state/_tables/usermeta.json b/tests/fixtures/native-bench-state/_tables/usermeta.json new file mode 100644 index 0000000..d93ce99 --- /dev/null +++ b/tests/fixtures/native-bench-state/_tables/usermeta.json @@ -0,0 +1,14 @@ +[ + { + "umeta_id": "1", + "user_id": "1", + "meta_key": "wp_capabilities", + "meta_value": "a:1:{s:13:\"administrator\";b:1;}" + }, + { + "umeta_id": "2", + "user_id": "1", + "meta_key": "wp_user_level", + "meta_value": "10" + } +] diff --git a/tests/fixtures/native-bench-state/_tables/users.json b/tests/fixtures/native-bench-state/_tables/users.json new file mode 100644 index 0000000..dbb8f1c --- /dev/null +++ b/tests/fixtures/native-bench-state/_tables/users.json @@ -0,0 +1,14 @@ +[ + { + "ID": "1", + "user_login": "admin", + "user_pass": "x", + "user_nicename": "admin", + "user_email": "admin@example.test", + "user_url": "", + "user_registered": "2026-01-01 00:00:00", + "user_activation_key": "", + "user_status": "0", + "display_name": "admin" + } +] diff --git a/tests/generate-native-bench-state.php b/tests/generate-native-bench-state.php new file mode 100644 index 0000000..ee5ec61 --- /dev/null +++ b/tests/generate-native-bench-state.php @@ -0,0 +1,19 @@ + Date: Sat, 29 Aug 2026 13:35:11 -0400 Subject: [PATCH 4/9] test: validate benchmark rig component paths --- rigs/mdi-native/rig.json | 1 + rigs/mdi-sqlite/rig.json | 1 + 2 files changed, 2 insertions(+) diff --git a/rigs/mdi-native/rig.json b/rigs/mdi-native/rig.json index 2a5a1dd..4e6b3ef 100644 --- a/rigs/mdi-native/rig.json +++ b/rigs/mdi-native/rig.json @@ -96,6 +96,7 @@ "label": "MDI plugin checkout exists", "file": "${components.markdown-database-integration.path}/markdown-database-integration.php", "component": "markdown-database-integration", + "component_path_contains": "markdown-database-integration", "remediation": "Set HOMEBOY_RIG_COMPONENT_PATH__MDI_NATIVE__MARKDOWN_DATABASE_INTEGRATION to an MDI checkout." } ], diff --git a/rigs/mdi-sqlite/rig.json b/rigs/mdi-sqlite/rig.json index a88216a..dd14554 100644 --- a/rigs/mdi-sqlite/rig.json +++ b/rigs/mdi-sqlite/rig.json @@ -75,6 +75,7 @@ "label": "MDI plugin checkout exists", "file": "${components.markdown-database-integration.path}/markdown-database-integration.php", "component": "markdown-database-integration", + "component_path_contains": "markdown-database-integration", "remediation": "Set HOMEBOY_RIG_COMPONENT_PATH__MDI_SQLITE__MARKDOWN_DATABASE_INTEGRATION to an MDI checkout." } ], From b65789609f15ac29556e6712d6068488d13b3424 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 29 Aug 2026 14:22:23 -0400 Subject: [PATCH 5/9] fix: execute production plugin predicates --- ...lass-wp-markdown-native-query-executor.php | 13 +++++ .../class-wp-markdown-native-query-parser.php | 52 +++++++++++++------ tests/bench/plugin-table-inventory.php | 8 ++- tests/smoke-native-inequality-query.php | 4 +- tests/smoke-native-plugin-schema-query.php | 5 ++ tests/smoke-native-query-parser.php | 12 ++++- 6 files changed, 73 insertions(+), 21 deletions(-) diff --git a/inc/native/class-wp-markdown-native-query-executor.php b/inc/native/class-wp-markdown-native-query-executor.php index 715f7a3..4289cad 100644 --- a/inc/native/class-wp-markdown-native-query-executor.php +++ b/inc/native/class-wp-markdown-native-query-executor.php @@ -558,6 +558,9 @@ function ( array $left, array $right ) use ( $plan, $sources ): int { private function allows_residual_scan( array $predicates, WP_Markdown_Native_Table_Schema $schema ): bool { $indexed = $this->indexed_columns( $schema ); foreach ( $predicates as $predicate ) { + if ( in_array( $predicate->operator(), array( 'OR', 'LOWER =' ), true ) ) { + continue; + } if ( $this->predicate_uses_like( $predicate ) ) { continue; } @@ -627,6 +630,11 @@ private function supports_predicate( WP_Markdown_Native_Table_Schema $schema, WP if ( in_array( $predicate->operator(), array( 'IS NULL', 'IS NOT NULL' ), true ) ) { return $schema->has_column( $predicate->column() ); } + if ( 'LOWER =' === $predicate->operator() ) { + return $schema->has_column( $predicate->column() ) + && 1 === count( $predicate->values() ) + && null !== WP_Markdown_Native_Runtime_Factory::normalize_ascii_ci( $predicate->values()[0] ); + } return $schema->allows_lookup( $predicate->column(), $predicate->operator(), $predicate->values() ) || $schema->allows_filter( $predicate->column(), $predicate->operator(), $predicate->values() ); } @@ -677,6 +685,11 @@ private function matches_predicate( array $row, WP_Markdown_Native_Query_Predica if ( 'IS NOT NULL' === $predicate->operator() ) { return null !== ( $row[ $predicate->column() ] ?? null ); } + if ( 'LOWER =' === $predicate->operator() ) { + $left = WP_Markdown_Native_Runtime_Factory::normalize_ascii_ci( $row[ $predicate->column() ] ?? null ); + $right = WP_Markdown_Native_Runtime_Factory::normalize_ascii_ci( $predicate->values()[0] ?? null ); + return null !== $left && null !== $right && $left === $right; + } $negated = in_array( $predicate->operator(), array( 'NOT IN', 'NOT LIKE' ), true ); if ( $negated && null === ( $row[ $predicate->column() ] ?? null ) ) { return false; diff --git a/inc/native/class-wp-markdown-native-query-parser.php b/inc/native/class-wp-markdown-native-query-parser.php index 69214a2..8feb3c2 100644 --- a/inc/native/class-wp-markdown-native-query-parser.php +++ b/inc/native/class-wp-markdown-native-query-parser.php @@ -405,9 +405,9 @@ private function is_on(): bool { * Parse a WHERE expression. * * AND binds tighter than OR, matching SQL. A disjunction is accepted only - * when every alternative is equality on the same column, which is the - * membership shape WordPress uses for `post_status` lists. Cross-column - * OR and inequality OR stay fail-closed. + * when every alternative is a supported equality predicate. Same-column + * alternatives collapse to membership; cross-column alternatives retain + * an explicit bounded disjunction. Inequality OR stays fail-closed. * * @return array */ @@ -439,24 +439,25 @@ private function coalesce_disjunction( array $groups, int $sql_offset ): array { if ( count( $likes ) === count( $groups ) ) { return array( new WP_Markdown_Native_SQL_Predicate( $likes[0]->column(), 'OR', array(), $likes ) ); } - $column = null; - $qualifier = null; - $values = array(); - $identifier = null; + $column = null; + $qualifier = null; + $values = array(); + $identifier = null; + $same_column = true; foreach ( $groups as $group ) { if ( 1 !== count( $group ) ) { throw new WP_Markdown_Native_SQL_Parse_Error( 'unsupported_or', $sql_offset, - 'mdi-native supports OR only as same-column equality or LIKE alternatives.' + 'mdi-native supports OR only as equality or LIKE alternatives.' ); } $predicate = $group[0]; - if ( ! in_array( $predicate->operator(), array( '=', 'IN', 'IS NULL' ), true ) ) { + if ( ! in_array( $predicate->operator(), array( '=', 'IN', 'IS NULL', 'LOWER =' ), true ) ) { throw new WP_Markdown_Native_SQL_Parse_Error( 'unsupported_or', $sql_offset, - 'mdi-native supports OR only as same-column equality or LIKE alternatives.' + 'mdi-native supports OR only as equality or LIKE alternatives.' ); } $name = $predicate->column()->name(); @@ -466,15 +467,14 @@ private function coalesce_disjunction( array $groups, int $sql_offset ): array { $qualifier = $qual; $identifier = $predicate->column(); } elseif ( $column !== $name || $qualifier !== $qual ) { - throw new WP_Markdown_Native_SQL_Parse_Error( - 'unsupported_or', - $sql_offset, - 'mdi-native supports OR only as same-column equality or LIKE alternatives.' - ); + $same_column = false; } $values = array_merge( $values, $predicate->values() ); } $alternatives = array_map( static fn( array $group ): WP_Markdown_Native_SQL_Predicate => $group[0], $groups ); + if ( ! $same_column ) { + return array( new WP_Markdown_Native_SQL_Predicate( $identifier, 'OR', array(), $alternatives ) ); + } foreach ( $alternatives as $alternative ) { if ( 'IS NULL' === $alternative->operator() ) { return array( new WP_Markdown_Native_SQL_Predicate( $identifier, 'OR', array(), $alternatives ) ); @@ -547,6 +547,9 @@ private function matches_function( string $function ): bool { } private function predicate(): WP_Markdown_Native_SQL_Predicate { + if ( $this->matches_function( 'LOWER' ) ) { + return $this->lower_equality_predicate(); + } $column = $this->identifier(); if ( $this->match_keyword( 'IS' ) ) { $operator = $this->match_keyword( 'NOT' ) ? 'IS NOT NULL' : 'IS NULL'; @@ -573,6 +576,25 @@ private function predicate(): WP_Markdown_Native_SQL_Predicate { return new WP_Markdown_Native_SQL_Predicate( $column, 'IN', $this->in_list() ); } + private function lower_equality_predicate(): WP_Markdown_Native_SQL_Predicate { + $this->unqualified_identifier(); + $this->expect_type( WP_Markdown_Native_SQL_Token::LEFT_PAREN ); + $column = $this->identifier(); + $this->expect_type( WP_Markdown_Native_SQL_Token::RIGHT_PAREN ); + $this->expect_type( WP_Markdown_Native_SQL_Token::EQUALS ); + if ( ! $this->matches_function( 'LOWER' ) ) { + $this->unsupported( $this->current() ); + } + $this->unqualified_identifier(); + $this->expect_type( WP_Markdown_Native_SQL_Token::LEFT_PAREN ); + $value = $this->literal(); + $this->expect_type( WP_Markdown_Native_SQL_Token::RIGHT_PAREN ); + if ( ! is_string( $value->value() ) || 1 === preg_match( '/[^\x00-\x7F]/', $value->value() ) ) { + throw new WP_Markdown_Native_SQL_Parse_Error( 'unsupported_literal', $value->sql_offset(), 'mdi-native LOWER equality requires an ASCII string literal.' ); + } + return new WP_Markdown_Native_SQL_Predicate( $column, 'LOWER =', array( $value ) ); + } + private function like_predicate( WP_Markdown_Native_SQL_Identifier $column, string $operator ): WP_Markdown_Native_SQL_Predicate { $pattern = $this->literal(); if ( ! is_string( $pattern->value() ) ) { diff --git a/tests/bench/plugin-table-inventory.php b/tests/bench/plugin-table-inventory.php index 97b8cd0..9a3e5d6 100644 --- a/tests/bench/plugin-table-inventory.php +++ b/tests/bench/plugin-table-inventory.php @@ -68,6 +68,7 @@ ), ARRAY_A ); + $task_error = (string) $wpdb->last_error; $sequence = count($all_rows); $replaced = $wpdb->replace($table, [ @@ -81,8 +82,11 @@ 'updated_at' => '2026-08-29 00:00:00', ]); - if (!is_array($all_rows) || !is_array($repo_rows) || !is_array($task_rows) || false === $replaced || '' !== (string) $wpdb->last_error) { - throw new RuntimeException('Plugin-table inventory workload failed: ' . (string) $wpdb->last_error); + if (!is_array($all_rows) || !is_array($repo_rows) || !is_array($task_rows) || false === $replaced || '' !== $task_error || '' !== (string) $wpdb->last_error) { + throw new RuntimeException('Plugin-table inventory workload failed: ' . ($task_error ?: (string) $wpdb->last_error)); + } + if (count($all_rows) < 500 || 20 !== count($repo_rows) || 5 !== count($task_rows)) { + throw new RuntimeException('Plugin-table inventory workload returned incorrect result counts.'); } return [ diff --git a/tests/smoke-native-inequality-query.php b/tests/smoke-native-inequality-query.php index 25d7c1f..502756a 100644 --- a/tests/smoke-native-inequality-query.php +++ b/tests/smoke-native-inequality-query.php @@ -138,8 +138,8 @@ function ids( WP_Markdown_Native_Query_Runtime $runtime, string $sql ): array { && 'unsupported_grammar' === $gt['reason'], 'admin status OR returns the visible statuses' => array( '21', '22' ) === $admin_or['ids'] && null === $admin_or['reason'], - 'cross-column OR fails closed' => false === $cross_or['return'] - && 'unsupported_or' === $cross_or['reason'], + 'cross-column equality OR returns either matching predicate' => array( '21', '22', '23', '24' ) === $cross_or['ids'] + && null === $cross_or['reason'], 'NOT IN excludes the listed statuses' => array( '21', '22' ) === $not_in['ids'] && null === $not_in['reason'], ); diff --git a/tests/smoke-native-plugin-schema-query.php b/tests/smoke-native-plugin-schema-query.php index 2e77158..9cdf1ee 100644 --- a/tests/smoke-native-plugin-schema-query.php +++ b/tests/smoke-native-plugin-schema-query.php @@ -84,6 +84,7 @@ function mdi_plugin_schema_remove_tree( string $root ): void { $secondary = $runtime->execute( new WP_Markdown_Query_Request( 'SELECT id, status FROM wp_plugin_jobs WHERE owner_id IN (7) ORDER BY id ASC LIMIT 2' ) ); $unfiltered = $runtime->execute( new WP_Markdown_Query_Request( 'SELECT id FROM wp_plugin_jobs LIMIT 2' ) ); $string_filter = $runtime->execute( new WP_Markdown_Query_Request( "SELECT id FROM wp_plugin_jobs WHERE status = 'QUEUED'" ) ); +$lower_cross_filter = $runtime->execute( new WP_Markdown_Query_Request( "SELECT id FROM wp_plugin_jobs WHERE owner_id = 8 OR LOWER(status) = LOWER('QUEUED') ORDER BY id ASC" ) ); $string_order = $runtime->execute( new WP_Markdown_Query_Request( 'SELECT id FROM wp_plugin_jobs ORDER BY status ASC' ) ); $no_identity = $runtime->execute( new WP_Markdown_Query_Request( 'SELECT value FROM wp_no_identity' ) ); $composite = $runtime->execute( new WP_Markdown_Query_Request( 'SELECT right_id FROM wp_composite WHERE left_id = 3' ) ); @@ -154,6 +155,10 @@ function mdi_plugin_schema_remove_tree( string $root ): void { && 'unsupported_lookup' === ( $string_filter->diagnostic()['reason'] ?? null ) && false === $string_order->return_value() && 'unsupported_order' === ( $string_order->diagnostic()['reason'] ?? null ), + 'explicit LOWER equality composes with a cross-column indexed predicate' => array( '2', '10' ) === array_map( + static fn( object $row ): string => $row->id, + $lower_cross_filter->wpdb_state()['last_result'] + ), 'schemas without a numeric primary identity remain unsupported' => false === $no_identity->return_value(), 'composite numeric plugin identities execute without table-specific code' => array( '7', '9' ) === array_map( static fn( object $row ): string => $row->right_id, diff --git a/tests/smoke-native-query-parser.php b/tests/smoke-native-query-parser.php index 04d4be6..8633c5f 100644 --- a/tests/smoke-native-query-parser.php +++ b/tests/smoke-native-query-parser.php @@ -35,6 +35,7 @@ $wordpress_admin_or_ast = $parser->parse_ast( "SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND ((wp_posts.post_type = 'post' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'future' OR wp_posts.post_status = 'draft' OR wp_posts.post_status = 'pending' OR wp_posts.post_status = 'private'))) ORDER BY wp_posts.post_date DESC LIMIT 0, 20" ); $wordpress_admin_or_plan = $wordpress_admin_or_ast instanceof WP_Markdown_Native_SQL_Select ? $parser->lower( $wordpress_admin_or_ast ) : $wordpress_admin_or_ast; $cross_column_or = $parser->parse( "SELECT ID FROM wp_posts WHERE post_type = 'post' OR post_status = 'publish'" ); +$lower_cross_column_or = $parser->parse( "SELECT ID FROM wp_posts WHERE guid = 'https://example.test/42' OR LOWER(post_name) = LOWER('BENCH-42')" ); $inequality_or = $parser->parse( "SELECT ID FROM wp_posts WHERE post_status <> 'trash' OR post_status <> 'auto-draft'" ); $like_ast = $parser->parse_ast( "SELECT ID FROM wp_posts WHERE post_title LIKE '%Hello%'" ); $like_plan = $like_ast instanceof WP_Markdown_Native_SQL_Select ? $parser->lower( $like_ast ) : $like_ast; @@ -149,8 +150,15 @@ && 2 === count( $wordpress_admin_or_plan->predicates() ) && 'IN' === $wordpress_admin_or_plan->predicates()[1]->operator() && array( 'publish', 'future', 'draft', 'pending', 'private' ) === $wordpress_admin_or_plan->predicates()[1]->values(), - 'cross-column OR fails closed' => false === $cross_column_or->return_value() - && 'unsupported_or' === ( $cross_column_or->diagnostic()['reason'] ?? null ), + 'cross-column equality OR retains bounded alternatives' => $cross_column_or instanceof WP_Markdown_Native_Query_Plan + && 'OR' === $cross_column_or->predicates()[0]->operator() + && array( 'post_type', 'post_status' ) === array_map( + static fn( WP_Markdown_Native_Query_Predicate $predicate ): string => $predicate->column(), + $cross_column_or->predicates()[0]->any() + ), + 'LOWER equality composes with cross-column OR' => $lower_cross_column_or instanceof WP_Markdown_Native_Query_Plan + && 'OR' === $lower_cross_column_or->predicates()[0]->operator() + && 'LOWER =' === $lower_cross_column_or->predicates()[0]->any()[1]->operator(), 'inequality OR fails closed' => false === $inequality_or->return_value() && 'unsupported_or' === ( $inequality_or->diagnostic()['reason'] ?? null ), 'composite ORDER BY keeps every key' => $composite_order_plan instanceof WP_Markdown_Native_Query_Plan From 74df4d3c17de724628fe09dd56c5270326d85391 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 29 Aug 2026 15:43:58 -0400 Subject: [PATCH 6/9] fix: validate generic text predicates --- ...lass-wp-markdown-native-schema-catalog.php | 25 +++++++++++-------- tests/smoke-native-plugin-schema-query.php | 18 +++++++------ 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/inc/native/class-wp-markdown-native-schema-catalog.php b/inc/native/class-wp-markdown-native-schema-catalog.php index bc7c786..d3d26f4 100644 --- a/inc/native/class-wp-markdown-native-schema-catalog.php +++ b/inc/native/class-wp-markdown-native-schema-catalog.php @@ -312,12 +312,21 @@ public static function indexed_snapshot_schema( } } $overlay = array( 'columns' => array(), 'natural_order' => $identity, 'order_columns' => $order_columns ); + $ascii = static fn( array $values ): bool => array() === array_filter( + $values, + static fn( mixed $value ): bool => ! is_string( $value ) || 1 === preg_match( '/[^\x00-\x7F]/', $value ) + ); foreach ( $definition['columns'] as $name => $column ) { - $overlay['columns'][ $name ] = array( - 'filter_operators' => self::is_integer( $column['type'] ) || self::is_decimal( $column['type'] ) - ? array( '=', 'IN', 'NOT IN', '<>' ) - : ( in_array( $column['type'], array( 'char', 'varchar', 'enum', 'set', 'tinytext', 'text', 'mediumtext', 'longtext' ), true ) ? array( 'LIKE', 'NOT LIKE' ) : array() ), - ); + if ( self::is_integer( $column['type'] ) || self::is_decimal( $column['type'] ) ) { + $overlay['columns'][ $name ] = array( 'filter_operators' => array( '=', 'IN', 'NOT IN', '<>' ) ); + } elseif ( in_array( $column['type'], array( 'char', 'varchar', 'enum', 'set', 'tinytext', 'text', 'mediumtext', 'longtext' ), true ) ) { + $overlay['columns'][ $name ] = array( + 'filter_operators' => array( '=', 'IN', 'NOT IN', '<>', 'LIKE', 'NOT LIKE' ), + 'filter_validator' => $ascii, + ); + } else { + $overlay['columns'][ $name ] = array( 'filter_operators' => array() ); + } } foreach ( $definition['indexes'] as $index ) { $unique = true === ( $index['unique'] ?? false ); @@ -332,12 +341,6 @@ public static function indexed_snapshot_schema( continue; } if ( in_array( $type, array( 'char', 'varchar', 'enum', 'set' ), true ) ) { - $ascii = static fn( array $values ): bool => array() === array_filter( - $values, - static fn( mixed $value ): bool => ! is_string( $value ) || 1 === preg_match( '/[^\x00-\x7F]/', $value ) - ); - $overlay['columns'][ $name ]['filter_operators'] = array( '=', 'IN', 'NOT IN', '<>', 'LIKE', 'NOT LIKE' ); - $overlay['columns'][ $name ]['filter_validator'] = $ascii; if ( $unique ) { $overlay['columns'][ $name ]['lookup_operators'] = array( '=', 'IN' ); $overlay['columns'][ $name ]['lookup_validator'] = $ascii; diff --git a/tests/smoke-native-plugin-schema-query.php b/tests/smoke-native-plugin-schema-query.php index 9cdf1ee..3346ef4 100644 --- a/tests/smoke-native-plugin-schema-query.php +++ b/tests/smoke-native-plugin-schema-query.php @@ -30,6 +30,8 @@ function mdi_plugin_schema_remove_tree( string $root ): void { . " `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n" . " `owner_id` bigint(20) unsigned NOT NULL DEFAULT '0',\n" . " `status` varchar(32) NOT NULL DEFAULT '',\n" + . " `task_url` text DEFAULT NULL,\n" + . " `owner_run_ref` varchar(191) DEFAULT NULL,\n" . " `payload` longtext DEFAULT NULL,\n" . " PRIMARY KEY (`id`),\n" . " KEY `owner_id` (`owner_id`)\n" @@ -39,9 +41,9 @@ function mdi_plugin_schema_remove_tree( string $root ): void { $root . '/_tables/plugin_jobs.json', json_encode( array( - array( 'id' => 10, 'owner_id' => '8', 'status' => 'running', 'payload' => null ), - array( 'id' => '2', 'owner_id' => 7, 'status' => 'queued', 'payload' => 'work' ), - array( 'id' => 1, 'owner_id' => '7', 'status' => 'done', 'payload' => '' ), + array( 'id' => 10, 'owner_id' => '8', 'status' => 'running', 'task_url' => 'https://example.test/issues/10', 'owner_run_ref' => 'BENCH-RUN-42', 'payload' => null ), + array( 'id' => '2', 'owner_id' => 7, 'status' => 'queued', 'task_url' => 'https://example.test/issues/42', 'owner_run_ref' => 'bench-run-2', 'payload' => 'work' ), + array( 'id' => 1, 'owner_id' => '7', 'status' => 'done', 'task_url' => null, 'owner_run_ref' => null, 'payload' => '' ), ), JSON_THROW_ON_ERROR ) @@ -84,7 +86,7 @@ function mdi_plugin_schema_remove_tree( string $root ): void { $secondary = $runtime->execute( new WP_Markdown_Query_Request( 'SELECT id, status FROM wp_plugin_jobs WHERE owner_id IN (7) ORDER BY id ASC LIMIT 2' ) ); $unfiltered = $runtime->execute( new WP_Markdown_Query_Request( 'SELECT id FROM wp_plugin_jobs LIMIT 2' ) ); $string_filter = $runtime->execute( new WP_Markdown_Query_Request( "SELECT id FROM wp_plugin_jobs WHERE status = 'QUEUED'" ) ); -$lower_cross_filter = $runtime->execute( new WP_Markdown_Query_Request( "SELECT id FROM wp_plugin_jobs WHERE owner_id = 8 OR LOWER(status) = LOWER('QUEUED') ORDER BY id ASC" ) ); +$lower_cross_filter = $runtime->execute( new WP_Markdown_Query_Request( "SELECT id FROM wp_plugin_jobs WHERE task_url = 'https://example.test/issues/42' OR LOWER(owner_run_ref) = LOWER('bench-run-42') ORDER BY id ASC" ) ); $string_order = $runtime->execute( new WP_Markdown_Query_Request( 'SELECT id FROM wp_plugin_jobs ORDER BY status ASC' ) ); $no_identity = $runtime->execute( new WP_Markdown_Query_Request( 'SELECT value FROM wp_no_identity' ) ); $composite = $runtime->execute( new WP_Markdown_Query_Request( 'SELECT right_id FROM wp_composite WHERE left_id = 3' ) ); @@ -109,8 +111,8 @@ function mdi_plugin_schema_remove_tree( string $root ): void { $root . '/_tables/plugin_jobs.json', json_encode( array( - array( 'id' => 2, 'owner_id' => 7, 'status' => 'first', 'payload' => null ), - array( 'id' => '2', 'owner_id' => '7', 'status' => 'duplicate', 'payload' => null ), + array( 'id' => 2, 'owner_id' => 7, 'status' => 'first', 'task_url' => null, 'owner_run_ref' => null, 'payload' => null ), + array( 'id' => '2', 'owner_id' => '7', 'status' => 'duplicate', 'task_url' => null, 'owner_run_ref' => null, 'payload' => null ), ), JSON_THROW_ON_ERROR ) @@ -126,7 +128,7 @@ function mdi_plugin_schema_remove_tree( string $root ): void { && 1 === $show_table_wildcard->return_value() && 1 === $show_table_escaped->return_value() && 0 === $show_missing_table->return_value(), - 'generic column introspection derives MySQL-visible schema rows from persisted DDL' => array( 'id', 'owner_id', 'status', 'payload' ) === array_map( + 'generic column introspection derives MySQL-visible schema rows from persisted DDL' => array( 'id', 'owner_id', 'status', 'task_url', 'owner_run_ref', 'payload' ) === array_map( static fn( object $row ): string => $row->Field, $describe->wpdb_state()['last_result'] ) @@ -155,7 +157,7 @@ function mdi_plugin_schema_remove_tree( string $root ): void { && 'unsupported_lookup' === ( $string_filter->diagnostic()['reason'] ?? null ) && false === $string_order->return_value() && 'unsupported_order' === ( $string_order->diagnostic()['reason'] ?? null ), - 'explicit LOWER equality composes with a cross-column indexed predicate' => array( '2', '10' ) === array_map( + 'ASCII text equality composes with explicit LOWER equality across columns' => array( '2', '10' ) === array_map( static fn( object $row ): string => $row->id, $lower_cross_filter->wpdb_state()['last_result'] ), From f4d0d4fc43d227283980a4d84f90987da8a35c05 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 29 Aug 2026 16:07:21 -0400 Subject: [PATCH 7/9] test: benchmark transactional plugin writes --- rigs/mdi-native/rig.json | 6 +- rigs/mdi-sqlite/rig.json | 6 +- tests/bench/transaction-heavy.php | 120 ++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 tests/bench/transaction-heavy.php diff --git a/rigs/mdi-native/rig.json b/rigs/mdi-native/rig.json index 4e6b3ef..265381d 100644 --- a/rigs/mdi-native/rig.json +++ b/rigs/mdi-native/rig.json @@ -76,6 +76,9 @@ }, { "path": "${package.root}/tests/bench/plugin-table-inventory.php" + }, + { + "path": "${package.root}/tests/bench/transaction-heavy.php" } ] }, @@ -86,7 +89,8 @@ "obsidian-bursty", "read-heavy", "wiki-hierarchy", - "plugin-table-inventory" + "plugin-table-inventory", + "transaction-heavy" ] }, "pipeline": { diff --git a/rigs/mdi-sqlite/rig.json b/rigs/mdi-sqlite/rig.json index dd14554..e7297bd 100644 --- a/rigs/mdi-sqlite/rig.json +++ b/rigs/mdi-sqlite/rig.json @@ -55,6 +55,9 @@ }, { "path": "${package.root}/tests/bench/plugin-table-inventory.php" + }, + { + "path": "${package.root}/tests/bench/transaction-heavy.php" } ] }, @@ -65,7 +68,8 @@ "obsidian-bursty", "read-heavy", "wiki-hierarchy", - "plugin-table-inventory" + "plugin-table-inventory", + "transaction-heavy" ] }, "pipeline": { diff --git a/tests/bench/transaction-heavy.php b/tests/bench/transaction-heavy.php new file mode 100644 index 0000000..7cbe84b --- /dev/null +++ b/tests/bench/transaction-heavy.php @@ -0,0 +1,120 @@ +prefix . 'bench_transactions'; + + $wpdb->query( + "CREATE TABLE IF NOT EXISTS {$table} ( + id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + run_id bigint(20) unsigned NOT NULL, + transaction_id bigint(20) unsigned NOT NULL, + sequence_no int(11) unsigned NOT NULL, + state varchar(32) NOT NULL DEFAULT 'inserted', + payload longtext DEFAULT NULL, + PRIMARY KEY (id), + UNIQUE KEY operation (run_id, transaction_id, sequence_no), + KEY run_id (run_id) + )" + ); + if ('' !== (string) $wpdb->last_error) { + throw new RuntimeException('Transaction-heavy schema setup failed: ' . (string) $wpdb->last_error); + } + + $run_id++; + $transactions = 20; + $writes_per_transaction = 6; + $committed_transactions = 0; + $rolled_back_transactions = 0; + + for ($transaction_id = 0; $transaction_id < $transactions; $transaction_id++) { + if (false === $wpdb->query('START TRANSACTION')) { + throw new RuntimeException('Transaction-heavy workload could not start a transaction: ' . (string) $wpdb->last_error); + } + + for ($sequence = 0; $sequence < $writes_per_transaction; $sequence++) { + $inserted = $wpdb->insert($table, [ + 'run_id' => $run_id, + 'transaction_id' => $transaction_id, + 'sequence_no' => $sequence, + 'state' => 'inserted', + 'payload' => wp_json_encode([ + 'run' => $run_id, + 'transaction' => $transaction_id, + 'sequence' => $sequence, + ]), + ]); + if (false === $inserted) { + $wpdb->query('ROLLBACK'); + throw new RuntimeException('Transaction-heavy insert failed: ' . (string) $wpdb->last_error); + } + } + + $updated = $wpdb->update( + $table, + ['state' => 'updated'], + ['run_id' => $run_id, 'transaction_id' => $transaction_id, 'sequence_no' => 0] + ); + if (1 !== $updated) { + $wpdb->query('ROLLBACK'); + throw new RuntimeException('Transaction-heavy update did not affect exactly one row: ' . (string) $wpdb->last_error); + } + + if (0 === $transaction_id % 4) { + if (false === $wpdb->query('ROLLBACK')) { + throw new RuntimeException('Transaction-heavy rollback failed: ' . (string) $wpdb->last_error); + } + $rolled_back_transactions++; + } else { + if (false === $wpdb->query('COMMIT')) { + throw new RuntimeException('Transaction-heavy commit failed: ' . (string) $wpdb->last_error); + } + $committed_transactions++; + } + } + + $committed_rows = (int) $wpdb->get_var( + $wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE run_id = %d", $run_id) + ); + $updated_rows = (int) $wpdb->get_var( + $wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE run_id = %d AND state = %s", $run_id, 'updated') + ); + $rolled_back_rows = (int) $wpdb->get_var( + $wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE run_id = %d AND transaction_id = %d", $run_id, 0) + ); + $expected_rows = $committed_transactions * $writes_per_transaction; + + if ('' !== (string) $wpdb->last_error + || $expected_rows !== $committed_rows + || $committed_transactions !== $updated_rows + || 0 !== $rolled_back_rows + ) { + throw new RuntimeException('Transaction-heavy workload returned incorrect commit or rollback state.'); + } + + return [ + 'metrics' => [ + 'transactions' => $transactions, + 'writes' => $transactions * $writes_per_transaction, + 'committed_transactions' => $committed_transactions, + 'rolled_back_transactions' => $rolled_back_transactions, + 'committed_rows' => $committed_rows, + 'updated_rows' => $updated_rows, + ], + 'metadata' => [ + 'query_shape' => 'plugin-table transactions with inserts, updates, commits, rollbacks, and verified final state', + 'backend' => $runtime['backend'], + 'wpdb_class' => $runtime['wpdb_class'], + ], + ]; +}; From f4b02cee6ad6dd6fc86e5f9fa7c94d44604a875d Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 29 Aug 2026 18:14:27 -0400 Subject: [PATCH 8/9] test: benchmark concurrent database reads --- rigs/mdi-native/rig.json | 32 ++++++++- rigs/mdi-sqlite/rig.json | 36 +++++++++- .../concurrent-read-endpoint.php | 65 +++++++++++++++++++ 3 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 tests/bench-fixtures/concurrent-read-endpoint.php diff --git a/rigs/mdi-native/rig.json b/rigs/mdi-native/rig.json index 265381d..a0e6438 100644 --- a/rigs/mdi-native/rig.json +++ b/rigs/mdi-native/rig.json @@ -20,10 +20,30 @@ "bench_env": { "BENCH_CORPUS_SIZE": "100" }, + "wordpress_runtime_workloads": [ + { + "id": "concurrent-read", + "source": "rig", + "overridesDiscovered": true, + "run": [ + { + "type": "external-http-load", + "url": "/wp-json/mdi-bench/v1/concurrent-read", + "requestCount": 20, + "concurrency": 4, + "expectedStatuses": [ + 200 + ], + "metric-prefix": "concurrent_read" + } + ] + } + ], "wp_config_defines": { "MARKDOWN_DB_BACKEND": "mdi-native", "MARKDOWN_DB_CONTENT_DIR": "/wordpress/wp-content/db", - "MARKDOWN_DB_STATE_DIR": "/wordpress/wp-content/db" + "MARKDOWN_DB_STATE_DIR": "/wordpress/wp-content/db", + "MDI_BENCH_EXPECTED_WPDB_CLASS": "WP_Markdown_Native_WPDB" }, "wp_codebox_bench_mounts": [ { @@ -39,6 +59,13 @@ "type": "directory", "mode": "readonly", "phase": "pre-install" + }, + { + "source": "${components.markdown-database-integration.path}/tests/bench-fixtures/concurrent-read-endpoint.php", + "target": "/wordpress/wp-content/mu-plugins/mdi-bench-concurrent-read.php", + "type": "file", + "mode": "readonly", + "phase": "pre-install" } ] } @@ -90,7 +117,8 @@ "read-heavy", "wiki-hierarchy", "plugin-table-inventory", - "transaction-heavy" + "transaction-heavy", + "concurrent-read" ] }, "pipeline": { diff --git a/rigs/mdi-sqlite/rig.json b/rigs/mdi-sqlite/rig.json index e7297bd..3dd9a21 100644 --- a/rigs/mdi-sqlite/rig.json +++ b/rigs/mdi-sqlite/rig.json @@ -19,7 +19,38 @@ "wordpress": { "bench_env": { "BENCH_CORPUS_SIZE": "100" - } + }, + "wordpress_runtime_workloads": [ + { + "id": "concurrent-read", + "source": "rig", + "overridesDiscovered": true, + "run": [ + { + "type": "external-http-load", + "url": "/wp-json/mdi-bench/v1/concurrent-read", + "requestCount": 20, + "concurrency": 4, + "expectedStatuses": [ + 200 + ], + "metric-prefix": "concurrent_read" + } + ] + } + ], + "wp_config_defines": { + "MDI_BENCH_EXPECTED_WPDB_CLASS": "WP_SQLite_DB" + }, + "wp_codebox_bench_mounts": [ + { + "source": "${components.markdown-database-integration.path}/tests/bench-fixtures/concurrent-read-endpoint.php", + "target": "/wordpress/wp-content/mu-plugins/mdi-bench-concurrent-read.php", + "type": "file", + "mode": "readonly", + "phase": "pre-install" + } + ] } } } @@ -69,7 +100,8 @@ "read-heavy", "wiki-hierarchy", "plugin-table-inventory", - "transaction-heavy" + "transaction-heavy", + "concurrent-read" ] }, "pipeline": { diff --git a/tests/bench-fixtures/concurrent-read-endpoint.php b/tests/bench-fixtures/concurrent-read-endpoint.php new file mode 100644 index 0000000..4ca89dd --- /dev/null +++ b/tests/bench-fixtures/concurrent-read-endpoint.php @@ -0,0 +1,65 @@ + 'GET', + 'permission_callback' => '__return_true', + 'callback' => static function () { + global $wpdb; + + $expected_class = defined('MDI_BENCH_EXPECTED_WPDB_CLASS') + ? (string) MDI_BENCH_EXPECTED_WPDB_CLASS + : ''; + $actual_class = is_object($wpdb) ? get_class($wpdb) : gettype($wpdb); + if ('' === $expected_class || $expected_class !== $actual_class) { + return new WP_Error( + 'mdi_bench_backend_mismatch', + sprintf('Expected %s, received %s.', $expected_class, $actual_class), + ['status' => 500] + ); + } + + $users = $wpdb->get_results( + "SELECT ID, user_login FROM {$wpdb->users} ORDER BY user_login ASC LIMIT 10", + ARRAY_A + ); + if ('' !== (string) $wpdb->last_error || !is_array($users) || [] === $users) { + return new WP_Error( + 'mdi_bench_query_failed', + 'The concurrent read query did not return the installed WordPress user.', + ['status' => 500] + ); + } + + foreach ($users as $user) { + if (!isset($user['ID'], $user['user_login']) || (int) $user['ID'] < 1 || '' === (string) $user['user_login']) { + return new WP_Error( + 'mdi_bench_invalid_result', + 'The concurrent read query returned an invalid user row.', + ['status' => 500] + ); + } + } + + return new WP_REST_Response( + [ + 'backend' => $actual_class, + 'rows' => count($users), + ], + 200 + ); + }, + ] + ); + } +); From a466ff6e9edc782e3a9c7f6e265f8f27f2e8c18d Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 29 Aug 2026 19:50:03 -0400 Subject: [PATCH 9/9] fix: keep runtime benchmark outside file profile --- rigs/mdi-native/rig.json | 3 +-- rigs/mdi-sqlite/rig.json | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/rigs/mdi-native/rig.json b/rigs/mdi-native/rig.json index a0e6438..ff8c330 100644 --- a/rigs/mdi-native/rig.json +++ b/rigs/mdi-native/rig.json @@ -117,8 +117,7 @@ "read-heavy", "wiki-hierarchy", "plugin-table-inventory", - "transaction-heavy", - "concurrent-read" + "transaction-heavy" ] }, "pipeline": { diff --git a/rigs/mdi-sqlite/rig.json b/rigs/mdi-sqlite/rig.json index 3dd9a21..6d634df 100644 --- a/rigs/mdi-sqlite/rig.json +++ b/rigs/mdi-sqlite/rig.json @@ -100,8 +100,7 @@ "read-heavy", "wiki-hierarchy", "plugin-table-inventory", - "transaction-heavy", - "concurrent-read" + "transaction-heavy" ] }, "pipeline": {