33/**
44 * Parser node backed by a native (Rust) AST.
55 *
6- * This subclass keeps the regular WP_Parser_Node API while delegating lazy AST
7- * reads to the optional native MySQL parser extension. The base node remains a
8- * plain PHP tree node for the polyfill parser.
6+ * Instances of this class are constructed exclusively by the native MySQL
7+ * parser extension: when the extension parses a query, it produces a tree of
8+ * `WP_MySQL_Native_Parser_Node` objects whose `$native_ast` and
9+ * `$native_node_index` fields point into a Rust-owned AST buffer. Read methods
10+ * (`get_start`, `has_child`, `get_children`, ...) delegate to the extension so
11+ * children are never materialized into PHP arrays unless something actually
12+ * asks for them.
13+ *
14+ * The hedge in those methods (`if ( $this->was_mutated() )`) is NOT a runtime
15+ * check for whether the native extension is loaded — if this class is in use,
16+ * the extension is loaded by definition. It checks whether THIS specific node
17+ * has been mutated from PHP. A node loses its native backing the first time
18+ * `append_child()` or `merge_fragment()` is called on it: those overrides
19+ * invoke `materialize_native_children()`, which copies the native children
20+ * into the inherited `$children` array and drops the native AST reference.
21+ * From that point on, the node is a plain PHP-backed `WP_Parser_Node` and the
22+ * read methods fall through to the parent implementation.
23+ *
24+ * Mutation from PHP is real and intentional — query rewriters in
25+ * `WP_PDO_MySQL_On_SQLite` (e.g. building synthetic `count(*)` expressions)
26+ * call `append_child()` on parsed nodes. The lazy-then-materialize design
27+ * keeps the fast path (read-only traversal) cheap while still allowing
28+ * mutation when callers need it.
929 */
1030class WP_MySQL_Native_Parser_Node extends WP_Parser_Node {
1131 private $ native_ast = null ;
@@ -19,13 +39,24 @@ public function __construct( $rule_id, $rule_name, $native_ast = null, $native_n
1939 $ this ->native_node_index = $ native_node_index ;
2040 }
2141
22- /** @inheritDoc */
42+ /**
43+ * Materializes any native children before mutating, then appends.
44+ *
45+ * Once a node is mutated, its native AST is no longer authoritative, so we
46+ * copy the native children into PHP storage first and drop the native
47+ * reference. Subsequent reads use the parent's PHP implementation.
48+ */
2349 public function append_child ( $ node ) {
2450 $ this ->materialize_native_children ();
2551 parent ::append_child ( $ node );
2652 }
2753
28- /** @inheritDoc */
54+ /**
55+ * Materializes any native children on both nodes before merging.
56+ *
57+ * @see self::append_child() for why materialization is required before
58+ * mutation.
59+ */
2960 public function merge_fragment ( $ node ) {
3061 $ this ->materialize_native_children ();
3162 if ( $ node instanceof self ) {
@@ -36,138 +67,158 @@ public function merge_fragment( $node ) {
3667
3768 /** @inheritDoc */
3869 public function has_child (): bool {
39- if ( $ this ->has_native_ast () ) {
40- return wp_sqlite_mysql_native_ast_has_child ( $ this -> native_ast , $ this -> native_node_index );
70+ if ( $ this ->was_mutated () ) {
71+ return parent :: has_child ( );
4172 }
4273 return wp_sqlite_mysql_native_ast_has_child ( $ this ->native_ast , $ this ->native_node_index );
4374 }
4475
4576 /** @inheritDoc */
4677 public function has_child_node ( ?string $ rule_name = null ): bool {
47- if ( $ this ->has_native_ast () ) {
48- return wp_sqlite_mysql_native_ast_has_child_node ( $ this -> native_ast , $ this -> native_node_index , $ rule_name );
78+ if ( $ this ->was_mutated () ) {
79+ return parent :: has_child_node ( $ rule_name );
4980 }
5081 return wp_sqlite_mysql_native_ast_has_child_node ( $ this ->native_ast , $ this ->native_node_index , $ rule_name );
5182 }
5283
5384 /** @inheritDoc */
5485 public function has_child_token ( ?int $ token_id = null ): bool {
55- if ( $ this ->has_native_ast () ) {
56- return wp_sqlite_mysql_native_ast_has_child_token ( $ this -> native_ast , $ this -> native_node_index , $ token_id );
86+ if ( $ this ->was_mutated () ) {
87+ return parent :: has_child_token ( $ token_id );
5788 }
5889 return wp_sqlite_mysql_native_ast_has_child_token ( $ this ->native_ast , $ this ->native_node_index , $ token_id );
5990 }
6091
6192 /** @inheritDoc */
6293 public function get_first_child () {
63- if ( $ this ->has_native_ast () ) {
64- return wp_sqlite_mysql_native_ast_get_first_child ( $ this -> native_ast , $ this -> native_node_index );
94+ if ( $ this ->was_mutated () ) {
95+ return parent :: get_first_child ( );
6596 }
6697 return wp_sqlite_mysql_native_ast_get_first_child ( $ this ->native_ast , $ this ->native_node_index );
6798 }
6899
69100 /** @inheritDoc */
70101 public function get_first_child_node ( ?string $ rule_name = null ): ?WP_Parser_Node {
71- if ( $ this ->has_native_ast () ) {
72- return wp_sqlite_mysql_native_ast_get_first_child_node ( $ this -> native_ast , $ this -> native_node_index , $ rule_name );
102+ if ( $ this ->was_mutated () ) {
103+ return parent :: get_first_child_node ( $ rule_name );
73104 }
74105 return wp_sqlite_mysql_native_ast_get_first_child_node ( $ this ->native_ast , $ this ->native_node_index , $ rule_name );
75106 }
76107
77108 /** @inheritDoc */
78109 public function get_first_child_token ( ?int $ token_id = null ): ?WP_Parser_Token {
79- if ( $ this ->has_native_ast () ) {
80- return wp_sqlite_mysql_native_ast_get_first_child_token ( $ this -> native_ast , $ this -> native_node_index , $ token_id );
110+ if ( $ this ->was_mutated () ) {
111+ return parent :: get_first_child_token ( $ token_id );
81112 }
82113 return wp_sqlite_mysql_native_ast_get_first_child_token ( $ this ->native_ast , $ this ->native_node_index , $ token_id );
83114 }
84115
85116 /** @inheritDoc */
86117 public function get_first_descendant_node ( ?string $ rule_name = null ): ?WP_Parser_Node {
87- if ( $ this ->has_native_ast () ) {
88- return wp_sqlite_mysql_native_ast_get_first_descendant_node ( $ this -> native_ast , $ this -> native_node_index , $ rule_name );
118+ if ( $ this ->was_mutated () ) {
119+ return parent :: get_first_descendant_node ( $ rule_name );
89120 }
90121 return wp_sqlite_mysql_native_ast_get_first_descendant_node ( $ this ->native_ast , $ this ->native_node_index , $ rule_name );
91122 }
92123
93124 /** @inheritDoc */
94125 public function get_first_descendant_token ( ?int $ token_id = null ): ?WP_Parser_Token {
95- if ( $ this ->has_native_ast () ) {
96- return wp_sqlite_mysql_native_ast_get_first_descendant_token ( $ this -> native_ast , $ this -> native_node_index , $ token_id );
126+ if ( $ this ->was_mutated () ) {
127+ return parent :: get_first_descendant_token ( $ token_id );
97128 }
98129 return wp_sqlite_mysql_native_ast_get_first_descendant_token ( $ this ->native_ast , $ this ->native_node_index , $ token_id );
99130 }
100131
101132 /** @inheritDoc */
102133 public function get_children (): array {
103- if ( $ this ->has_native_ast () ) {
104- return wp_sqlite_mysql_native_ast_get_children ( $ this -> native_ast , $ this -> native_node_index );
134+ if ( $ this ->was_mutated () ) {
135+ return parent :: get_children ( );
105136 }
106137 return wp_sqlite_mysql_native_ast_get_children ( $ this ->native_ast , $ this ->native_node_index );
107138 }
108139
109140 /** @inheritDoc */
110141 public function get_child_nodes ( ?string $ rule_name = null ): array {
111- if ( $ this ->has_native_ast () ) {
112- return wp_sqlite_mysql_native_ast_get_child_nodes ( $ this -> native_ast , $ this -> native_node_index , $ rule_name );
142+ if ( $ this ->was_mutated () ) {
143+ return parent :: get_child_nodes ( $ rule_name );
113144 }
114145 return wp_sqlite_mysql_native_ast_get_child_nodes ( $ this ->native_ast , $ this ->native_node_index , $ rule_name );
115146 }
116147
117148 /** @inheritDoc */
118149 public function get_child_tokens ( ?int $ token_id = null ): array {
119- if ( $ this ->has_native_ast () ) {
120- return wp_sqlite_mysql_native_ast_get_child_tokens ( $ this -> native_ast , $ this -> native_node_index , $ token_id );
150+ if ( $ this ->was_mutated () ) {
151+ return parent :: get_child_tokens ( $ token_id );
121152 }
122153 return wp_sqlite_mysql_native_ast_get_child_tokens ( $ this ->native_ast , $ this ->native_node_index , $ token_id );
123154 }
124155
125156 /** @inheritDoc */
126157 public function get_descendants (): array {
127- if ( $ this ->has_native_ast () ) {
128- return wp_sqlite_mysql_native_ast_get_descendants ( $ this -> native_ast , $ this -> native_node_index );
158+ if ( $ this ->was_mutated () ) {
159+ return parent :: get_descendants ( );
129160 }
130161 return wp_sqlite_mysql_native_ast_get_descendants ( $ this ->native_ast , $ this ->native_node_index );
131162 }
132163
133164 /** @inheritDoc */
134165 public function get_descendant_nodes ( ?string $ rule_name = null ): array {
135- if ( $ this ->has_native_ast () ) {
136- return wp_sqlite_mysql_native_ast_get_descendant_nodes ( $ this -> native_ast , $ this -> native_node_index , $ rule_name );
166+ if ( $ this ->was_mutated () ) {
167+ return parent :: get_descendant_nodes ( $ rule_name );
137168 }
138169 return wp_sqlite_mysql_native_ast_get_descendant_nodes ( $ this ->native_ast , $ this ->native_node_index , $ rule_name );
139170 }
140171
141172 /** @inheritDoc */
142173 public function get_descendant_tokens ( ?int $ token_id = null ): array {
143- if ( $ this ->has_native_ast () ) {
144- return wp_sqlite_mysql_native_ast_get_descendant_tokens ( $ this -> native_ast , $ this -> native_node_index , $ token_id );
174+ if ( $ this ->was_mutated () ) {
175+ return parent :: get_descendant_tokens ( $ token_id );
145176 }
146177 return wp_sqlite_mysql_native_ast_get_descendant_tokens ( $ this ->native_ast , $ this ->native_node_index , $ token_id );
147178 }
148179
149180 /** @inheritDoc */
150181 public function get_start (): int {
151- if ( $ this ->has_native_ast () ) {
152- return wp_sqlite_mysql_native_ast_get_start ( $ this -> native_ast , $ this -> native_node_index );
182+ if ( $ this ->was_mutated () ) {
183+ return parent :: get_start ( );
153184 }
154185 return wp_sqlite_mysql_native_ast_get_start ( $ this ->native_ast , $ this ->native_node_index );
155186 }
156187
157188 /** @inheritDoc */
158189 public function get_length (): int {
159- if ( $ this ->has_native_ast () ) {
160- return wp_sqlite_mysql_native_ast_get_length ( $ this -> native_ast , $ this -> native_node_index );
190+ if ( $ this ->was_mutated () ) {
191+ return parent :: get_length ( );
161192 }
162193 return wp_sqlite_mysql_native_ast_get_length ( $ this ->native_ast , $ this ->native_node_index );
163194 }
164195
165- private function has_native_ast (): bool {
166- return null !== $ this ->native_ast ;
167- }
168-
196+ /**
197+ * Indicates whether this node has been mutated from PHP.
198+ *
199+ * Returns false for freshly-parsed nodes whose children still live in the
200+ * Rust-owned AST buffer; returns true once `append_child()` or
201+ * `merge_fragment()` has copied the children into the inherited
202+ * `$children` array and dropped the native AST reference.
203+ *
204+ * This is a per-instance state check, not a check for whether the native
205+ * extension is loaded.
206+ */
207+ private function was_mutated (): bool {
208+ return $ this ->was_mutated ;
209+ }
210+
211+ /**
212+ * Copies native children into the inherited PHP $children array and drops
213+ * the native AST reference for this node.
214+ *
215+ * Called before any mutation (append_child, merge_fragment) so the node's
216+ * authoritative state lives in PHP from that point on. After this runs,
217+ * was_mutated() returns true and read methods fall through to the parent
218+ * WP_Parser_Node implementation.
219+ */
169220 private function materialize_native_children (): void {
170- if ( ! $ this ->has_native_ast () ) {
221+ if ( $ this ->was_mutated ) {
171222 return ;
172223 }
173224
0 commit comments