From c5068658735a7dc95de900033fbc8aa61193b477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 30 Apr 2026 14:40:22 +0200 Subject: [PATCH 1/8] Rename native node helper to was_mutated() The previous helper name (has_unmaterialized_native_ast) implied a runtime check for native-extension presence. It's actually a per-instance state flag tracking whether this node's children have been copied into PHP. was_mutated() reads that intent more directly. --- .../class-wp-mysql-native-parser-node.php | 48 +++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php b/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php index e796bf68..aa448e79 100644 --- a/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php +++ b/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php @@ -3,11 +3,29 @@ /** * Parser node backed by a native (Rust) AST. * - * Constructed by the native MySQL parser extension. Read methods delegate - * into the Rust-owned AST so children are never copied into PHP unless a - * caller actually walks the tree. On the first mutation (append_child or - * merge_fragment), the node materializes its children into the inherited - * `$children` array and behaves like a plain WP_Parser_Node from then on. + * Instances of this class are constructed exclusively by the native MySQL + * parser extension: when the extension parses a query, it produces a tree of + * `WP_MySQL_Native_Parser_Node` objects whose `$native_ast` and + * `$native_node_index` fields point into a Rust-owned AST buffer. Read methods + * (`get_start`, `has_child`, `get_children`, ...) delegate to the extension so + * children are never materialized into PHP arrays unless something actually + * asks for them. + * + * Read methods eagerly call `materialize_native_children()` — once the + * children have been copied into PHP, `was_mutated()` returns true and the + * call falls through to the parent implementation. The `was_mutated` flag is + * NOT a runtime check for whether the native extension is loaded — if this + * class is in use, the extension is loaded by definition. It tracks whether + * THIS specific node has had its children pulled into the inherited + * `$children` array (which happens on first read or first mutation via + * `append_child()` / `merge_fragment()`). From that point on, the node is a + * plain PHP-backed `WP_Parser_Node`. + * + * Mutation from PHP is real and intentional — query rewriters in + * `WP_PDO_MySQL_On_SQLite` (e.g. building synthetic `count(*)` expressions) + * call `append_child()` on parsed nodes. The lazy-then-materialize design + * keeps the fast path (read-only traversal) cheap while still allowing + * mutation when callers need it. */ class WP_MySQL_Native_Parser_Node extends WP_Parser_Node { private $native_ast = null; @@ -164,10 +182,30 @@ public function get_length(): int { return wp_sqlite_mysql_native_ast_get_length( $this->native_ast, $this->native_node_index ); } + /** + * Indicates whether this node has been mutated from PHP. + * + * Returns false for freshly-parsed nodes whose children still live in the + * Rust-owned AST buffer; returns true once `append_child()` or + * `merge_fragment()` has copied the children into the inherited + * `$children` array and dropped the native AST reference. + * + * This is a per-instance state check, not a check for whether the native + * extension is loaded. + */ private function was_mutated(): bool { return $this->was_mutated; } + /** + * Copies native children into the inherited PHP $children array and drops + * the native AST reference for this node. + * + * Called before any mutation (append_child, merge_fragment) so the node's + * authoritative state lives in PHP from that point on. After this runs, + * was_mutated() returns true and read methods fall through to the parent + * WP_Parser_Node implementation. + */ private function materialize_native_children(): void { if ( $this->was_mutated ) { return; From 0a6addd7544b112935b5bded953ddcec933130af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 30 Apr 2026 13:51:43 +0200 Subject: [PATCH 2/8] Add lazy native parser node facade When a native parser is in use, expose query results through a node class that defers child materialization until callers actually walk the tree. The base WP_Parser_Node::$children visibility is loosened to protected so the facade can populate it on demand. --- .../class-wp-mysql-native-parser-node.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php b/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php index aa448e79..b29bbee3 100644 --- a/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php +++ b/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php @@ -11,15 +11,15 @@ * children are never materialized into PHP arrays unless something actually * asks for them. * - * Read methods eagerly call `materialize_native_children()` — once the - * children have been copied into PHP, `was_mutated()` returns true and the - * call falls through to the parent implementation. The `was_mutated` flag is - * NOT a runtime check for whether the native extension is loaded — if this - * class is in use, the extension is loaded by definition. It tracks whether - * THIS specific node has had its children pulled into the inherited - * `$children` array (which happens on first read or first mutation via - * `append_child()` / `merge_fragment()`). From that point on, the node is a - * plain PHP-backed `WP_Parser_Node`. + * The hedge in those methods (`if ( $this->was_mutated() )`) is NOT a runtime + * check for whether the native extension is loaded — if this class is in use, + * the extension is loaded by definition. It checks whether THIS specific node + * has been mutated from PHP. A node loses its native backing the first time + * `append_child()` or `merge_fragment()` is called on it: those overrides + * invoke `materialize_native_children()`, which copies the native children + * into the inherited `$children` array and drops the native AST reference. + * From that point on, the node is a plain PHP-backed `WP_Parser_Node` and the + * read methods fall through to the parent implementation. * * Mutation from PHP is real and intentional — query rewriters in * `WP_PDO_MySQL_On_SQLite` (e.g. building synthetic `count(*)` expressions) From 8c5eb3f8b4cfe6a4727f512dbde61296981f1254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 30 Apr 2026 13:51:43 +0200 Subject: [PATCH 3/8] Add lazy native parser node facade When a native parser is in use, expose query results through a node class that defers child materialization until callers actually walk the tree. The base WP_Parser_Node::$children visibility is loosened to protected so the facade can populate it on demand. --- .../class-wp-mysql-native-parser-node.php | 120 ++++++------------ 1 file changed, 40 insertions(+), 80 deletions(-) diff --git a/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php b/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php index b29bbee3..e6f78fcc 100644 --- a/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php +++ b/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php @@ -3,29 +3,9 @@ /** * Parser node backed by a native (Rust) AST. * - * Instances of this class are constructed exclusively by the native MySQL - * parser extension: when the extension parses a query, it produces a tree of - * `WP_MySQL_Native_Parser_Node` objects whose `$native_ast` and - * `$native_node_index` fields point into a Rust-owned AST buffer. Read methods - * (`get_start`, `has_child`, `get_children`, ...) delegate to the extension so - * children are never materialized into PHP arrays unless something actually - * asks for them. - * - * The hedge in those methods (`if ( $this->was_mutated() )`) is NOT a runtime - * check for whether the native extension is loaded — if this class is in use, - * the extension is loaded by definition. It checks whether THIS specific node - * has been mutated from PHP. A node loses its native backing the first time - * `append_child()` or `merge_fragment()` is called on it: those overrides - * invoke `materialize_native_children()`, which copies the native children - * into the inherited `$children` array and drops the native AST reference. - * From that point on, the node is a plain PHP-backed `WP_Parser_Node` and the - * read methods fall through to the parent implementation. - * - * Mutation from PHP is real and intentional — query rewriters in - * `WP_PDO_MySQL_On_SQLite` (e.g. building synthetic `count(*)` expressions) - * call `append_child()` on parsed nodes. The lazy-then-materialize design - * keeps the fast path (read-only traversal) cheap while still allowing - * mutation when callers need it. + * This subclass keeps the regular WP_Parser_Node API while delegating lazy AST + * reads to the optional native MySQL parser extension. The base node remains a + * plain PHP tree node for the polyfill parser. */ class WP_MySQL_Native_Parser_Node extends WP_Parser_Node { private $native_ast = null; @@ -56,158 +36,138 @@ public function merge_fragment( $node ) { /** @inheritDoc */ public function has_child(): bool { - if ( $this->was_mutated() ) { - return parent::has_child(); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_has_child( $this->native_ast, $this->native_node_index ); } return wp_sqlite_mysql_native_ast_has_child( $this->native_ast, $this->native_node_index ); } /** @inheritDoc */ public function has_child_node( ?string $rule_name = null ): bool { - if ( $this->was_mutated() ) { - return parent::has_child_node( $rule_name ); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_has_child_node( $this->native_ast, $this->native_node_index, $rule_name ); } return wp_sqlite_mysql_native_ast_has_child_node( $this->native_ast, $this->native_node_index, $rule_name ); } /** @inheritDoc */ public function has_child_token( ?int $token_id = null ): bool { - if ( $this->was_mutated() ) { - return parent::has_child_token( $token_id ); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_has_child_token( $this->native_ast, $this->native_node_index, $token_id ); } return wp_sqlite_mysql_native_ast_has_child_token( $this->native_ast, $this->native_node_index, $token_id ); } /** @inheritDoc */ public function get_first_child() { - if ( $this->was_mutated() ) { - return parent::get_first_child(); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_get_first_child( $this->native_ast, $this->native_node_index ); } return wp_sqlite_mysql_native_ast_get_first_child( $this->native_ast, $this->native_node_index ); } /** @inheritDoc */ public function get_first_child_node( ?string $rule_name = null ): ?WP_Parser_Node { - if ( $this->was_mutated() ) { - return parent::get_first_child_node( $rule_name ); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_get_first_child_node( $this->native_ast, $this->native_node_index, $rule_name ); } return wp_sqlite_mysql_native_ast_get_first_child_node( $this->native_ast, $this->native_node_index, $rule_name ); } /** @inheritDoc */ public function get_first_child_token( ?int $token_id = null ): ?WP_Parser_Token { - if ( $this->was_mutated() ) { - return parent::get_first_child_token( $token_id ); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_get_first_child_token( $this->native_ast, $this->native_node_index, $token_id ); } return wp_sqlite_mysql_native_ast_get_first_child_token( $this->native_ast, $this->native_node_index, $token_id ); } /** @inheritDoc */ public function get_first_descendant_node( ?string $rule_name = null ): ?WP_Parser_Node { - if ( $this->was_mutated() ) { - return parent::get_first_descendant_node( $rule_name ); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_get_first_descendant_node( $this->native_ast, $this->native_node_index, $rule_name ); } return wp_sqlite_mysql_native_ast_get_first_descendant_node( $this->native_ast, $this->native_node_index, $rule_name ); } /** @inheritDoc */ public function get_first_descendant_token( ?int $token_id = null ): ?WP_Parser_Token { - if ( $this->was_mutated() ) { - return parent::get_first_descendant_token( $token_id ); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_get_first_descendant_token( $this->native_ast, $this->native_node_index, $token_id ); } return wp_sqlite_mysql_native_ast_get_first_descendant_token( $this->native_ast, $this->native_node_index, $token_id ); } /** @inheritDoc */ public function get_children(): array { - if ( $this->was_mutated() ) { - return parent::get_children(); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_get_children( $this->native_ast, $this->native_node_index ); } return wp_sqlite_mysql_native_ast_get_children( $this->native_ast, $this->native_node_index ); } /** @inheritDoc */ public function get_child_nodes( ?string $rule_name = null ): array { - if ( $this->was_mutated() ) { - return parent::get_child_nodes( $rule_name ); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_get_child_nodes( $this->native_ast, $this->native_node_index, $rule_name ); } return wp_sqlite_mysql_native_ast_get_child_nodes( $this->native_ast, $this->native_node_index, $rule_name ); } /** @inheritDoc */ public function get_child_tokens( ?int $token_id = null ): array { - if ( $this->was_mutated() ) { - return parent::get_child_tokens( $token_id ); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_get_child_tokens( $this->native_ast, $this->native_node_index, $token_id ); } return wp_sqlite_mysql_native_ast_get_child_tokens( $this->native_ast, $this->native_node_index, $token_id ); } /** @inheritDoc */ public function get_descendants(): array { - if ( $this->was_mutated() ) { - return parent::get_descendants(); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_get_descendants( $this->native_ast, $this->native_node_index ); } return wp_sqlite_mysql_native_ast_get_descendants( $this->native_ast, $this->native_node_index ); } /** @inheritDoc */ public function get_descendant_nodes( ?string $rule_name = null ): array { - if ( $this->was_mutated() ) { - return parent::get_descendant_nodes( $rule_name ); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_get_descendant_nodes( $this->native_ast, $this->native_node_index, $rule_name ); } return wp_sqlite_mysql_native_ast_get_descendant_nodes( $this->native_ast, $this->native_node_index, $rule_name ); } /** @inheritDoc */ public function get_descendant_tokens( ?int $token_id = null ): array { - if ( $this->was_mutated() ) { - return parent::get_descendant_tokens( $token_id ); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_get_descendant_tokens( $this->native_ast, $this->native_node_index, $token_id ); } return wp_sqlite_mysql_native_ast_get_descendant_tokens( $this->native_ast, $this->native_node_index, $token_id ); } /** @inheritDoc */ public function get_start(): int { - if ( $this->was_mutated() ) { - return parent::get_start(); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_get_start( $this->native_ast, $this->native_node_index ); } return wp_sqlite_mysql_native_ast_get_start( $this->native_ast, $this->native_node_index ); } /** @inheritDoc */ public function get_length(): int { - if ( $this->was_mutated() ) { - return parent::get_length(); + if ( $this->has_native_ast() ) { + return wp_sqlite_mysql_native_ast_get_length( $this->native_ast, $this->native_node_index ); } return wp_sqlite_mysql_native_ast_get_length( $this->native_ast, $this->native_node_index ); } - /** - * Indicates whether this node has been mutated from PHP. - * - * Returns false for freshly-parsed nodes whose children still live in the - * Rust-owned AST buffer; returns true once `append_child()` or - * `merge_fragment()` has copied the children into the inherited - * `$children` array and dropped the native AST reference. - * - * This is a per-instance state check, not a check for whether the native - * extension is loaded. - */ - private function was_mutated(): bool { - return $this->was_mutated; - } - - /** - * Copies native children into the inherited PHP $children array and drops - * the native AST reference for this node. - * - * Called before any mutation (append_child, merge_fragment) so the node's - * authoritative state lives in PHP from that point on. After this runs, - * was_mutated() returns true and read methods fall through to the parent - * WP_Parser_Node implementation. - */ + private function has_native_ast(): bool { + return null !== $this->native_ast; + } + private function materialize_native_children(): void { - if ( $this->was_mutated ) { + if ( ! $this->has_native_ast() ) { return; } From e815c2bc70464be0eb81ac6e3994fc444281eee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 30 Apr 2026 13:51:43 +0200 Subject: [PATCH 4/8] Add lazy native parser node facade When a native parser is in use, expose query results through a node class that defers child materialization until callers actually walk the tree. The base WP_Parser_Node::$children visibility is loosened to protected so the facade can populate it on demand. --- .../class-wp-mysql-native-parser-node.php | 135 ++++++++++++------ 1 file changed, 93 insertions(+), 42 deletions(-) diff --git a/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php b/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php index e6f78fcc..071a60e7 100644 --- a/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php +++ b/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php @@ -3,9 +3,29 @@ /** * Parser node backed by a native (Rust) AST. * - * This subclass keeps the regular WP_Parser_Node API while delegating lazy AST - * reads to the optional native MySQL parser extension. The base node remains a - * plain PHP tree node for the polyfill parser. + * Instances of this class are constructed exclusively by the native MySQL + * parser extension: when the extension parses a query, it produces a tree of + * `WP_MySQL_Native_Parser_Node` objects whose `$native_ast` and + * `$native_node_index` fields point into a Rust-owned AST buffer. Read methods + * (`get_start`, `has_child`, `get_children`, ...) delegate to the extension so + * children are never materialized into PHP arrays unless something actually + * asks for them. + * + * The hedge in those methods (`if ( $this->was_mutated() )`) is NOT a runtime + * check for whether the native extension is loaded — if this class is in use, + * the extension is loaded by definition. It checks whether THIS specific node + * has been mutated from PHP. A node loses its native backing the first time + * `append_child()` or `merge_fragment()` is called on it: those overrides + * invoke `materialize_native_children()`, which copies the native children + * into the inherited `$children` array and drops the native AST reference. + * From that point on, the node is a plain PHP-backed `WP_Parser_Node` and the + * read methods fall through to the parent implementation. + * + * Mutation from PHP is real and intentional — query rewriters in + * `WP_PDO_MySQL_On_SQLite` (e.g. building synthetic `count(*)` expressions) + * call `append_child()` on parsed nodes. The lazy-then-materialize design + * keeps the fast path (read-only traversal) cheap while still allowing + * mutation when callers need it. */ class WP_MySQL_Native_Parser_Node extends WP_Parser_Node { private $native_ast = null; @@ -19,13 +39,24 @@ public function __construct( $rule_id, $rule_name, $native_ast = null, $native_n $this->native_node_index = $native_node_index; } - /** @inheritDoc */ + /** + * Materializes any native children before mutating, then appends. + * + * Once a node is mutated, its native AST is no longer authoritative, so we + * copy the native children into PHP storage first and drop the native + * reference. Subsequent reads use the parent's PHP implementation. + */ public function append_child( $node ) { $this->materialize_native_children(); parent::append_child( $node ); } - /** @inheritDoc */ + /** + * Materializes any native children on both nodes before merging. + * + * @see self::append_child() for why materialization is required before + * mutation. + */ public function merge_fragment( $node ) { $this->materialize_native_children(); if ( $node instanceof self ) { @@ -36,138 +67,158 @@ public function merge_fragment( $node ) { /** @inheritDoc */ public function has_child(): bool { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_has_child( $this->native_ast, $this->native_node_index ); + if ( $this->was_mutated() ) { + return parent::has_child(); } return wp_sqlite_mysql_native_ast_has_child( $this->native_ast, $this->native_node_index ); } /** @inheritDoc */ public function has_child_node( ?string $rule_name = null ): bool { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_has_child_node( $this->native_ast, $this->native_node_index, $rule_name ); + if ( $this->was_mutated() ) { + return parent::has_child_node( $rule_name ); } return wp_sqlite_mysql_native_ast_has_child_node( $this->native_ast, $this->native_node_index, $rule_name ); } /** @inheritDoc */ public function has_child_token( ?int $token_id = null ): bool { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_has_child_token( $this->native_ast, $this->native_node_index, $token_id ); + if ( $this->was_mutated() ) { + return parent::has_child_token( $token_id ); } return wp_sqlite_mysql_native_ast_has_child_token( $this->native_ast, $this->native_node_index, $token_id ); } /** @inheritDoc */ public function get_first_child() { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_get_first_child( $this->native_ast, $this->native_node_index ); + if ( $this->was_mutated() ) { + return parent::get_first_child(); } return wp_sqlite_mysql_native_ast_get_first_child( $this->native_ast, $this->native_node_index ); } /** @inheritDoc */ public function get_first_child_node( ?string $rule_name = null ): ?WP_Parser_Node { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_get_first_child_node( $this->native_ast, $this->native_node_index, $rule_name ); + if ( $this->was_mutated() ) { + return parent::get_first_child_node( $rule_name ); } return wp_sqlite_mysql_native_ast_get_first_child_node( $this->native_ast, $this->native_node_index, $rule_name ); } /** @inheritDoc */ public function get_first_child_token( ?int $token_id = null ): ?WP_Parser_Token { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_get_first_child_token( $this->native_ast, $this->native_node_index, $token_id ); + if ( $this->was_mutated() ) { + return parent::get_first_child_token( $token_id ); } return wp_sqlite_mysql_native_ast_get_first_child_token( $this->native_ast, $this->native_node_index, $token_id ); } /** @inheritDoc */ public function get_first_descendant_node( ?string $rule_name = null ): ?WP_Parser_Node { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_get_first_descendant_node( $this->native_ast, $this->native_node_index, $rule_name ); + if ( $this->was_mutated() ) { + return parent::get_first_descendant_node( $rule_name ); } return wp_sqlite_mysql_native_ast_get_first_descendant_node( $this->native_ast, $this->native_node_index, $rule_name ); } /** @inheritDoc */ public function get_first_descendant_token( ?int $token_id = null ): ?WP_Parser_Token { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_get_first_descendant_token( $this->native_ast, $this->native_node_index, $token_id ); + if ( $this->was_mutated() ) { + return parent::get_first_descendant_token( $token_id ); } return wp_sqlite_mysql_native_ast_get_first_descendant_token( $this->native_ast, $this->native_node_index, $token_id ); } /** @inheritDoc */ public function get_children(): array { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_get_children( $this->native_ast, $this->native_node_index ); + if ( $this->was_mutated() ) { + return parent::get_children(); } return wp_sqlite_mysql_native_ast_get_children( $this->native_ast, $this->native_node_index ); } /** @inheritDoc */ public function get_child_nodes( ?string $rule_name = null ): array { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_get_child_nodes( $this->native_ast, $this->native_node_index, $rule_name ); + if ( $this->was_mutated() ) { + return parent::get_child_nodes( $rule_name ); } return wp_sqlite_mysql_native_ast_get_child_nodes( $this->native_ast, $this->native_node_index, $rule_name ); } /** @inheritDoc */ public function get_child_tokens( ?int $token_id = null ): array { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_get_child_tokens( $this->native_ast, $this->native_node_index, $token_id ); + if ( $this->was_mutated() ) { + return parent::get_child_tokens( $token_id ); } return wp_sqlite_mysql_native_ast_get_child_tokens( $this->native_ast, $this->native_node_index, $token_id ); } /** @inheritDoc */ public function get_descendants(): array { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_get_descendants( $this->native_ast, $this->native_node_index ); + if ( $this->was_mutated() ) { + return parent::get_descendants(); } return wp_sqlite_mysql_native_ast_get_descendants( $this->native_ast, $this->native_node_index ); } /** @inheritDoc */ public function get_descendant_nodes( ?string $rule_name = null ): array { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_get_descendant_nodes( $this->native_ast, $this->native_node_index, $rule_name ); + if ( $this->was_mutated() ) { + return parent::get_descendant_nodes( $rule_name ); } return wp_sqlite_mysql_native_ast_get_descendant_nodes( $this->native_ast, $this->native_node_index, $rule_name ); } /** @inheritDoc */ public function get_descendant_tokens( ?int $token_id = null ): array { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_get_descendant_tokens( $this->native_ast, $this->native_node_index, $token_id ); + if ( $this->was_mutated() ) { + return parent::get_descendant_tokens( $token_id ); } return wp_sqlite_mysql_native_ast_get_descendant_tokens( $this->native_ast, $this->native_node_index, $token_id ); } /** @inheritDoc */ public function get_start(): int { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_get_start( $this->native_ast, $this->native_node_index ); + if ( $this->was_mutated() ) { + return parent::get_start(); } return wp_sqlite_mysql_native_ast_get_start( $this->native_ast, $this->native_node_index ); } /** @inheritDoc */ public function get_length(): int { - if ( $this->has_native_ast() ) { - return wp_sqlite_mysql_native_ast_get_length( $this->native_ast, $this->native_node_index ); + if ( $this->was_mutated() ) { + return parent::get_length(); } return wp_sqlite_mysql_native_ast_get_length( $this->native_ast, $this->native_node_index ); } - private function has_native_ast(): bool { - return null !== $this->native_ast; - } - + /** + * Indicates whether this node has been mutated from PHP. + * + * Returns false for freshly-parsed nodes whose children still live in the + * Rust-owned AST buffer; returns true once `append_child()` or + * `merge_fragment()` has copied the children into the inherited + * `$children` array and dropped the native AST reference. + * + * This is a per-instance state check, not a check for whether the native + * extension is loaded. + */ + private function was_mutated(): bool { + return $this->was_mutated; + } + + /** + * Copies native children into the inherited PHP $children array and drops + * the native AST reference for this node. + * + * Called before any mutation (append_child, merge_fragment) so the node's + * authoritative state lives in PHP from that point on. After this runs, + * was_mutated() returns true and read methods fall through to the parent + * WP_Parser_Node implementation. + */ private function materialize_native_children(): void { - if ( ! $this->has_native_ast() ) { + if ( $this->was_mutated ) { return; } From 20426074688c527a8a8a839b156407c0d7dc56a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 28 Apr 2026 17:39:41 +0200 Subject: [PATCH 5/8] Speed up native AST materialization --- packages/php-ext-wp-mysql-parser/src/lib.rs | 130 ++++++++++++++++---- 1 file changed, 106 insertions(+), 24 deletions(-) diff --git a/packages/php-ext-wp-mysql-parser/src/lib.rs b/packages/php-ext-wp-mysql-parser/src/lib.rs index 835dea1b..e6c76311 100644 --- a/packages/php-ext-wp-mysql-parser/src/lib.rs +++ b/packages/php-ext-wp-mysql-parser/src/lib.rs @@ -93,10 +93,19 @@ fn update_object_property( } fn create_mysql_token(sql_zval: &Zval, token: TokenInfo, no_backslash: bool) -> PhpResult { + let classes = php_classes()?; + create_mysql_token_with_classes(sql_zval, token, no_backslash, &classes) +} + +fn create_mysql_token_with_classes( + sql_zval: &Zval, + token: TokenInfo, + no_backslash: bool, + classes: &PhpClasses, +) -> PhpResult { let id = token.id; let start = i64::try_from(token.start).map_err(php_error)?; let length = i64::try_from(token.end.saturating_sub(token.start)).map_err(php_error)?; - let classes = php_classes()?; let mut object = classes.mysql_token.new(); update_object_property(&mut object, classes.parser_token, "id", id)?; @@ -940,7 +949,7 @@ enum ParserTokenSource { } impl ParserTokenSource { - fn create_php_token(&self, index: usize) -> PhpResult { + fn create_php_token_with_classes(&self, index: usize, classes: &PhpClasses) -> PhpResult { match self { Self::Php(tokens) => tokens .get(index) @@ -955,7 +964,7 @@ impl ParserTokenSource { .get(index) .copied() .ok_or_else(|| php_error("Parser token index is out of range"))?; - create_mysql_token(sql_zval, token, *no_backslash) + create_mysql_token_with_classes(sql_zval, token, *no_backslash, classes) } } } @@ -1020,6 +1029,7 @@ struct NativeAstNode { children: Vec, first_token: Option, last_token: Option, + descendant_count: usize, } struct NativeAstArena { @@ -1049,10 +1059,12 @@ impl NativeAstArena { let index = self.nodes.len(); let mut first_token = None; let mut last_token = None; + let mut descendant_count = 0; for child in &children { match child { NativeAstChild::Node(child_index) => { if let Some(node) = self.nodes.get(*child_index) { + descendant_count += 1 + node.descendant_count; if first_token.is_none() { first_token = node.first_token; } @@ -1066,6 +1078,7 @@ impl NativeAstArena { first_token = Some(*token_index); } last_token = Some(*token_index); + descendant_count += 1; } } } @@ -1075,11 +1088,21 @@ impl NativeAstArena { children, first_token, last_token, + descendant_count, }); index } fn create_php_ast(&self, native_ast_zval: &Zval) -> PhpResult { + let classes = php_classes()?; + self.create_php_ast_with_classes(native_ast_zval, &classes) + } + + fn create_php_ast_with_classes( + &self, + native_ast_zval: &Zval, + classes: &PhpClasses, + ) -> PhpResult { match self.root { NativeAstRoot::No => Ok(Zval::null()), NativeAstRoot::Empty => { @@ -1087,14 +1110,22 @@ impl NativeAstArena { zval.set_bool(true); Ok(zval) } - NativeAstRoot::Node(index) => self.create_php_node(native_ast_zval, index), - NativeAstRoot::Token(index) => self.token_source.create_php_token(index), + NativeAstRoot::Node(index) => { + self.create_php_node_with_classes(native_ast_zval, index, classes) + } + NativeAstRoot::Token(index) => self + .token_source + .create_php_token_with_classes(index, classes), } } - fn create_php_node(&self, native_ast_zval: &Zval, index: usize) -> PhpResult { + fn create_php_node_with_classes( + &self, + native_ast_zval: &Zval, + index: usize, + classes: &PhpClasses, + ) -> PhpResult { let node = self.node(index)?; - let classes = php_classes()?; let mut object = classes.native_parser_node.new(); let rule_name = self .grammar @@ -1137,10 +1168,19 @@ impl NativeAstArena { .ok_or_else(|| php_error("Native AST node index is out of range")) } - fn child_to_zval(&self, native_ast_zval: &Zval, child: NativeAstChild) -> PhpResult { + fn child_to_zval_with_classes( + &self, + native_ast_zval: &Zval, + child: NativeAstChild, + classes: &PhpClasses, + ) -> PhpResult { match child { - NativeAstChild::Node(index) => self.create_php_node(native_ast_zval, index), - NativeAstChild::Token(index) => self.token_source.create_php_token(index), + NativeAstChild::Node(index) => { + self.create_php_node_with_classes(native_ast_zval, index, classes) + } + NativeAstChild::Token(index) => self + .token_source + .create_php_token_with_classes(index, classes), } } @@ -1172,8 +1212,9 @@ impl NativeAstArena { } fn descendant_stack(&self, index: usize) -> PhpResult> { - let mut stack = self.node(index)?.children.clone(); - stack.reverse(); + let node = self.node(index)?; + let mut stack = Vec::with_capacity(node.descendant_count); + stack.extend(node.children.iter().rev().copied()); Ok(stack) } } @@ -1238,6 +1279,7 @@ pub fn wp_sqlite_mysql_native_ast_get_first_child( node_index: i64, ) -> PhpResult { let ast = native_ast(native_ast_zval)?; + let classes = php_classes()?; let Some(child) = ast .arena .node(native_ast_node_index(node_index)?)? @@ -1247,7 +1289,8 @@ pub fn wp_sqlite_mysql_native_ast_get_first_child( else { return Ok(Zval::null()); }; - ast.arena.child_to_zval(native_ast_zval, child) + ast.arena + .child_to_zval_with_classes(native_ast_zval, child, &classes) } #[php_function] @@ -1257,9 +1300,12 @@ pub fn wp_sqlite_mysql_native_ast_get_first_child_node( rule_name: Option, ) -> PhpResult { let ast = native_ast(native_ast_zval)?; + let classes = php_classes()?; for child in &ast.arena.node(native_ast_node_index(node_index)?)?.children { if ast.arena.child_node_matches(*child, rule_name.as_deref()) { - return ast.arena.child_to_zval(native_ast_zval, *child); + return ast + .arena + .child_to_zval_with_classes(native_ast_zval, *child, &classes); } } Ok(Zval::null()) @@ -1272,9 +1318,12 @@ pub fn wp_sqlite_mysql_native_ast_get_first_child_token( token_id: Option, ) -> PhpResult { let ast = native_ast(native_ast_zval)?; + let classes = php_classes()?; for child in &ast.arena.node(native_ast_node_index(node_index)?)?.children { if ast.arena.child_token_matches(*child, token_id) { - return ast.arena.child_to_zval(native_ast_zval, *child); + return ast + .arena + .child_to_zval_with_classes(native_ast_zval, *child, &classes); } } Ok(Zval::null()) @@ -1287,12 +1336,15 @@ pub fn wp_sqlite_mysql_native_ast_get_first_descendant_node( rule_name: Option, ) -> PhpResult { let ast = native_ast(native_ast_zval)?; + let classes = php_classes()?; let mut stack = ast .arena .descendant_stack(native_ast_node_index(node_index)?)?; while let Some(child) = stack.pop() { if ast.arena.child_node_matches(child, rule_name.as_deref()) { - return ast.arena.child_to_zval(native_ast_zval, child); + return ast + .arena + .child_to_zval_with_classes(native_ast_zval, child, &classes); } if let NativeAstChild::Node(index) = child { for child in ast.arena.node(index)?.children.iter().rev() { @@ -1310,12 +1362,15 @@ pub fn wp_sqlite_mysql_native_ast_get_first_descendant_token( token_id: Option, ) -> PhpResult { let ast = native_ast(native_ast_zval)?; + let classes = php_classes()?; let mut stack = ast .arena .descendant_stack(native_ast_node_index(node_index)?)?; while let Some(child) = stack.pop() { if ast.arena.child_token_matches(child, token_id) { - return ast.arena.child_to_zval(native_ast_zval, child); + return ast + .arena + .child_to_zval_with_classes(native_ast_zval, child, &classes); } if let NativeAstChild::Node(index) = child { for child in ast.arena.node(index)?.children.iter().rev() { @@ -1332,12 +1387,16 @@ pub fn wp_sqlite_mysql_native_ast_get_children( node_index: i64, ) -> PhpResult> { let ast = native_ast(native_ast_zval)?; + let classes = php_classes()?; ast.arena .node(native_ast_node_index(node_index)?)? .children .iter() .copied() - .map(|child| ast.arena.child_to_zval(native_ast_zval, child)) + .map(|child| { + ast.arena + .child_to_zval_with_classes(native_ast_zval, child, &classes) + }) .collect() } @@ -1348,13 +1407,17 @@ pub fn wp_sqlite_mysql_native_ast_get_child_nodes( rule_name: Option, ) -> PhpResult> { let ast = native_ast(native_ast_zval)?; + let classes = php_classes()?; ast.arena .node(native_ast_node_index(node_index)?)? .children .iter() .copied() .filter(|child| ast.arena.child_node_matches(*child, rule_name.as_deref())) - .map(|child| ast.arena.child_to_zval(native_ast_zval, child)) + .map(|child| { + ast.arena + .child_to_zval_with_classes(native_ast_zval, child, &classes) + }) .collect() } @@ -1365,13 +1428,17 @@ pub fn wp_sqlite_mysql_native_ast_get_child_tokens( token_id: Option, ) -> PhpResult> { let ast = native_ast(native_ast_zval)?; + let classes = php_classes()?; ast.arena .node(native_ast_node_index(node_index)?)? .children .iter() .copied() .filter(|child| ast.arena.child_token_matches(*child, token_id)) - .map(|child| ast.arena.child_to_zval(native_ast_zval, child)) + .map(|child| { + ast.arena + .child_to_zval_with_classes(native_ast_zval, child, &classes) + }) .collect() } @@ -1381,12 +1448,17 @@ pub fn wp_sqlite_mysql_native_ast_get_descendants( node_index: i64, ) -> PhpResult> { let ast = native_ast(native_ast_zval)?; - let mut descendants = Vec::new(); + let classes = php_classes()?; + let root = ast.arena.node(native_ast_node_index(node_index)?)?; + let mut descendants = Vec::with_capacity(root.descendant_count); let mut stack = ast .arena .descendant_stack(native_ast_node_index(node_index)?)?; while let Some(child) = stack.pop() { - descendants.push(ast.arena.child_to_zval(native_ast_zval, child)?); + descendants.push( + ast.arena + .child_to_zval_with_classes(native_ast_zval, child, &classes)?, + ); if let NativeAstChild::Node(index) = child { for child in ast.arena.node(index)?.children.iter().rev() { stack.push(*child); @@ -1403,13 +1475,18 @@ pub fn wp_sqlite_mysql_native_ast_get_descendant_nodes( rule_name: Option, ) -> PhpResult> { let ast = native_ast(native_ast_zval)?; + let classes = php_classes()?; let mut descendants = Vec::new(); let mut stack = ast .arena .descendant_stack(native_ast_node_index(node_index)?)?; while let Some(child) = stack.pop() { if ast.arena.child_node_matches(child, rule_name.as_deref()) { - descendants.push(ast.arena.child_to_zval(native_ast_zval, child)?); + descendants.push(ast.arena.child_to_zval_with_classes( + native_ast_zval, + child, + &classes, + )?); } if let NativeAstChild::Node(index) = child { for child in ast.arena.node(index)?.children.iter().rev() { @@ -1427,13 +1504,18 @@ pub fn wp_sqlite_mysql_native_ast_get_descendant_tokens( token_id: Option, ) -> PhpResult> { let ast = native_ast(native_ast_zval)?; + let classes = php_classes()?; let mut descendants = Vec::new(); let mut stack = ast .arena .descendant_stack(native_ast_node_index(node_index)?)?; while let Some(child) = stack.pop() { if ast.arena.child_token_matches(child, token_id) { - descendants.push(ast.arena.child_to_zval(native_ast_zval, child)?); + descendants.push(ast.arena.child_to_zval_with_classes( + native_ast_zval, + child, + &classes, + )?); } if let NativeAstChild::Node(index) = child { for child in ast.arena.node(index)?.children.iter().rev() { From 13666592d6e1df29f614ea76816f162f2a2aaba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 29 Apr 2026 03:10:51 +0200 Subject: [PATCH 6/8] Run full native suites after bulk materialization --- .github/workflows/mysql-parser-extension-tests.yml | 8 +++++++- .github/workflows/wp-tests-phpunit.yml | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mysql-parser-extension-tests.yml b/.github/workflows/mysql-parser-extension-tests.yml index 5f8e49d3..cbe93fad 100644 --- a/.github/workflows/mysql-parser-extension-tests.yml +++ b/.github/workflows/mysql-parser-extension-tests.yml @@ -81,7 +81,9 @@ jobs: exit( 1 ); } ' - ./vendor/bin/phpunit -c ./phpunit.xml.dist tests/mysql/WP_MySQL_Lexer_Tests.php tests/parser/WP_Parser_Node_Tests.php + + - name: Run PHPUnit tests with parser extension + run: php -d extension="$GITHUB_WORKSPACE/packages/mysql-on-sqlite/ext/wp-mysql-parser/target/debug/libwp_mysql_parser.so" ./vendor/bin/phpunit -c ./phpunit.xml.dist working-directory: packages/mysql-on-sqlite sqlite-driver-extension-tests: @@ -149,3 +151,7 @@ jobs: exit( 1 ); } ' + + - name: Run PHPUnit tests with SQLite driver using parser extension + run: php -d extension="$GITHUB_WORKSPACE/packages/mysql-on-sqlite/ext/wp-mysql-parser/target/debug/libwp_mysql_parser.so" ./vendor/bin/phpunit -c ./phpunit.xml.dist + working-directory: packages/mysql-on-sqlite diff --git a/.github/workflows/wp-tests-phpunit.yml b/.github/workflows/wp-tests-phpunit.yml index f2c8faf6..31db5ef0 100644 --- a/.github/workflows/wp-tests-phpunit.yml +++ b/.github/workflows/wp-tests-phpunit.yml @@ -54,8 +54,8 @@ jobs: - name: Build and load parser extension in WordPress PHP containers run: bash .github/workflows/wp-tests-phpunit-native-extension-setup.sh - - name: Verify WordPress uses parser extension - run: cd wordpress && node tools/local-env/scripts/docker.js run --rm php php /var/www/native-verify-extension.php + - name: Run WordPress PHPUnit tests with parser extension + run: node .github/workflows/wp-tests-phpunit-run.js - name: Stop Docker containers if: always() From 2b04a241378ee8ad824b42dfd162bf791d5dcee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 29 Apr 2026 15:34:18 +0200 Subject: [PATCH 7/8] Reuse SQLite driver MySQL parser --- .../src/mysql/class-wp-mysql-parser.php | 11 ++++++++ .../sqlite/class-wp-pdo-mysql-on-sqlite.php | 27 +++++++++++++++++-- packages/php-ext-wp-mysql-parser/src/lib.rs | 12 +++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/mysql-on-sqlite/src/mysql/class-wp-mysql-parser.php b/packages/mysql-on-sqlite/src/mysql/class-wp-mysql-parser.php index f291064e..69282b9c 100644 --- a/packages/mysql-on-sqlite/src/mysql/class-wp-mysql-parser.php +++ b/packages/mysql-on-sqlite/src/mysql/class-wp-mysql-parser.php @@ -8,6 +8,17 @@ class WP_MySQL_Parser extends WP_Parser { */ private $current_ast; + /** + * Reset this parser with a new token stream. + * + * @param array $tokens The parser tokens. + */ + public function reset_tokens( array $tokens ): void { + $this->tokens = $tokens; + $this->position = 0; + $this->current_ast = null; + } + /** * Parse the next query from the input SQL string. * diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php index b3653ffe..a8ddf146 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php @@ -410,6 +410,13 @@ class WP_PDO_MySQL_On_SQLite extends PDO { */ private static $mysql_grammar; + /** + * A reusable parser instance for MySQL queries. + * + * @var WP_MySQL_Parser|null + */ + private $mysql_parser = null; + /** * The main database name. * @@ -1160,11 +1167,27 @@ public function create_parser( string $query ): WP_MySQL_Parser { ); if ( $lexer instanceof WP_MySQL_Native_Lexer ) { $tokens = $lexer->native_token_stream(); - return new WP_MySQL_Parser( self::$mysql_grammar, $tokens ); + return $this->reset_or_create_parser( $tokens ); } $tokens = $lexer->remaining_tokens(); - return new WP_MySQL_Parser( self::$mysql_grammar, $tokens ); + return $this->reset_or_create_parser( $tokens ); + } + + /** + * Reset the reusable parser with new tokens or create it on first use. + * + * @param array|object $tokens Parser tokens. + * @return WP_MySQL_Parser A parser initialized for the token stream. + */ + private function reset_or_create_parser( $tokens ): WP_MySQL_Parser { + if ( null === $this->mysql_parser || ! method_exists( $this->mysql_parser, 'reset_tokens' ) ) { + $this->mysql_parser = new WP_MySQL_Parser( self::$mysql_grammar, $tokens ); + } else { + $this->mysql_parser->reset_tokens( $tokens ); + } + + return $this->mysql_parser; } /** diff --git a/packages/php-ext-wp-mysql-parser/src/lib.rs b/packages/php-ext-wp-mysql-parser/src/lib.rs index e6c76311..909ec379 100644 --- a/packages/php-ext-wp-mysql-parser/src/lib.rs +++ b/packages/php-ext-wp-mysql-parser/src/lib.rs @@ -1587,6 +1587,18 @@ impl WpMySqlNativeParser { }) } + pub fn reset_tokens(&mut self, tokens: &mut Zval) -> PhpResult<()> { + let (token_source, token_ids) = export_tokens(tokens)?; + + self.token_source = Arc::new(token_source); + self.token_ids = token_ids; + self.position = 0; + self.current_ast = None; + self.current_php_ast = None; + + Ok(()) + } + pub fn parse(&mut self) -> PhpResult { stacker::maybe_grow(STACK_RED_ZONE, STACK_GROW_SIZE, || { let ast = self.parse_native_ast()?; From 12e984fcfc9c748dc47c3d13c97aa32c2d79e56e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 30 Apr 2026 14:43:04 +0200 Subject: [PATCH 8/8] Simplify native parser node docblocks at tip --- .../mysql-parser-extension-tests.yml | 5 +- .../class-wp-mysql-native-parser-node.php | 63 +++---------------- 2 files changed, 10 insertions(+), 58 deletions(-) diff --git a/.github/workflows/mysql-parser-extension-tests.yml b/.github/workflows/mysql-parser-extension-tests.yml index cbe93fad..234bb5c7 100644 --- a/.github/workflows/mysql-parser-extension-tests.yml +++ b/.github/workflows/mysql-parser-extension-tests.yml @@ -81,9 +81,10 @@ jobs: exit( 1 ); } ' + working-directory: packages/mysql-on-sqlite - name: Run PHPUnit tests with parser extension - run: php -d extension="$GITHUB_WORKSPACE/packages/mysql-on-sqlite/ext/wp-mysql-parser/target/debug/libwp_mysql_parser.so" ./vendor/bin/phpunit -c ./phpunit.xml.dist + run: php -d extension="$GITHUB_WORKSPACE/packages/php-ext-wp-mysql-parser/target/debug/libwp_mysql_parser.so" ./vendor/bin/phpunit -c ./phpunit.xml.dist working-directory: packages/mysql-on-sqlite sqlite-driver-extension-tests: @@ -153,5 +154,5 @@ jobs: ' - name: Run PHPUnit tests with SQLite driver using parser extension - run: php -d extension="$GITHUB_WORKSPACE/packages/mysql-on-sqlite/ext/wp-mysql-parser/target/debug/libwp_mysql_parser.so" ./vendor/bin/phpunit -c ./phpunit.xml.dist + run: php -d extension="$GITHUB_WORKSPACE/packages/php-ext-wp-mysql-parser/target/debug/libwp_mysql_parser.so" ./vendor/bin/phpunit -c ./phpunit.xml.dist working-directory: packages/mysql-on-sqlite diff --git a/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php b/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php index 071a60e7..e796bf68 100644 --- a/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php +++ b/packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php @@ -3,29 +3,11 @@ /** * Parser node backed by a native (Rust) AST. * - * Instances of this class are constructed exclusively by the native MySQL - * parser extension: when the extension parses a query, it produces a tree of - * `WP_MySQL_Native_Parser_Node` objects whose `$native_ast` and - * `$native_node_index` fields point into a Rust-owned AST buffer. Read methods - * (`get_start`, `has_child`, `get_children`, ...) delegate to the extension so - * children are never materialized into PHP arrays unless something actually - * asks for them. - * - * The hedge in those methods (`if ( $this->was_mutated() )`) is NOT a runtime - * check for whether the native extension is loaded — if this class is in use, - * the extension is loaded by definition. It checks whether THIS specific node - * has been mutated from PHP. A node loses its native backing the first time - * `append_child()` or `merge_fragment()` is called on it: those overrides - * invoke `materialize_native_children()`, which copies the native children - * into the inherited `$children` array and drops the native AST reference. - * From that point on, the node is a plain PHP-backed `WP_Parser_Node` and the - * read methods fall through to the parent implementation. - * - * Mutation from PHP is real and intentional — query rewriters in - * `WP_PDO_MySQL_On_SQLite` (e.g. building synthetic `count(*)` expressions) - * call `append_child()` on parsed nodes. The lazy-then-materialize design - * keeps the fast path (read-only traversal) cheap while still allowing - * mutation when callers need it. + * Constructed by the native MySQL parser extension. Read methods delegate + * into the Rust-owned AST so children are never copied into PHP unless a + * caller actually walks the tree. On the first mutation (append_child or + * merge_fragment), the node materializes its children into the inherited + * `$children` array and behaves like a plain WP_Parser_Node from then on. */ class WP_MySQL_Native_Parser_Node extends WP_Parser_Node { private $native_ast = null; @@ -39,24 +21,13 @@ public function __construct( $rule_id, $rule_name, $native_ast = null, $native_n $this->native_node_index = $native_node_index; } - /** - * Materializes any native children before mutating, then appends. - * - * Once a node is mutated, its native AST is no longer authoritative, so we - * copy the native children into PHP storage first and drop the native - * reference. Subsequent reads use the parent's PHP implementation. - */ + /** @inheritDoc */ public function append_child( $node ) { $this->materialize_native_children(); parent::append_child( $node ); } - /** - * Materializes any native children on both nodes before merging. - * - * @see self::append_child() for why materialization is required before - * mutation. - */ + /** @inheritDoc */ public function merge_fragment( $node ) { $this->materialize_native_children(); if ( $node instanceof self ) { @@ -193,30 +164,10 @@ public function get_length(): int { return wp_sqlite_mysql_native_ast_get_length( $this->native_ast, $this->native_node_index ); } - /** - * Indicates whether this node has been mutated from PHP. - * - * Returns false for freshly-parsed nodes whose children still live in the - * Rust-owned AST buffer; returns true once `append_child()` or - * `merge_fragment()` has copied the children into the inherited - * `$children` array and dropped the native AST reference. - * - * This is a per-instance state check, not a check for whether the native - * extension is loaded. - */ private function was_mutated(): bool { return $this->was_mutated; } - /** - * Copies native children into the inherited PHP $children array and drops - * the native AST reference for this node. - * - * Called before any mutation (append_child, merge_fragment) so the node's - * authoritative state lives in PHP from that point on. After this runs, - * was_mutated() returns true and read methods fall through to the parent - * WP_Parser_Node implementation. - */ private function materialize_native_children(): void { if ( $this->was_mutated ) { return;