11<?php
22
33/**
4- * Native- backed parser node .
4+ * 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->has_unmaterialized_native_ast() )`)
15+ * is NOT a runtime check for whether the native extension is loaded — if this
16+ * class is in use, the extension is loaded by definition. It checks whether
17+ * THIS specific node still has an authoritative native AST behind it. A node
18+ * loses its native backing the first time it is mutated from PHP via
19+ * `append_child()` or `merge_fragment()`: those overrides call
20+ * `materialize_native_children()`, which copies the native children into the
21+ * inherited `$children` array and then drops the native AST reference. From
22+ * that point on, the node is a plain PHP-backed `WP_Parser_Node` and the read
23+ * methods fall through to the parent implementation.
24+ *
25+ * Mutation from PHP is real and intentional — query rewriters in
26+ * `WP_PDO_MySQL_On_SQLite` (e.g. building synthetic `count(*)` expressions)
27+ * call `append_child()` on parsed nodes. The lazy-then-materialize design
28+ * keeps the fast path (read-only traversal) cheap while still allowing
29+ * mutation when callers need it.
930 */
1031class WP_MySQL_Native_Parser_Node extends WP_Parser_Node {
1132 private $ native_ast = null ;
@@ -18,13 +39,24 @@ public function __construct( $rule_id, $rule_name, $native_ast = null, $native_n
1839 $ this ->native_node_index = $ native_node_index ;
1940 }
2041
21- /** @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+ */
2249 public function append_child ( $ node ) {
2350 $ this ->materialize_native_children ();
2451 parent ::append_child ( $ node );
2552 }
2653
27- /** @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+ */
2860 public function merge_fragment ( $ node ) {
2961 $ this ->materialize_native_children ();
3062 if ( $ node instanceof self ) {
@@ -35,138 +67,158 @@ public function merge_fragment( $node ) {
3567
3668 /** @inheritDoc */
3769 public function has_child (): bool {
38- if ( $ this ->has_native_ast () ) {
70+ if ( $ this ->has_unmaterialized_native_ast () ) {
3971 return wp_sqlite_mysql_native_ast_has_child ( $ this ->native_ast , $ this ->native_node_index );
4072 }
4173 return parent ::has_child ();
4274 }
4375
4476 /** @inheritDoc */
4577 public function has_child_node ( ?string $ rule_name = null ): bool {
46- if ( $ this ->has_native_ast () ) {
78+ if ( $ this ->has_unmaterialized_native_ast () ) {
4779 return wp_sqlite_mysql_native_ast_has_child_node ( $ this ->native_ast , $ this ->native_node_index , $ rule_name );
4880 }
4981 return parent ::has_child_node ( $ rule_name );
5082 }
5183
5284 /** @inheritDoc */
5385 public function has_child_token ( ?int $ token_id = null ): bool {
54- if ( $ this ->has_native_ast () ) {
86+ if ( $ this ->has_unmaterialized_native_ast () ) {
5587 return wp_sqlite_mysql_native_ast_has_child_token ( $ this ->native_ast , $ this ->native_node_index , $ token_id );
5688 }
5789 return parent ::has_child_token ( $ token_id );
5890 }
5991
6092 /** @inheritDoc */
6193 public function get_first_child () {
62- if ( $ this ->has_native_ast () ) {
94+ if ( $ this ->has_unmaterialized_native_ast () ) {
6395 return wp_sqlite_mysql_native_ast_get_first_child ( $ this ->native_ast , $ this ->native_node_index );
6496 }
6597 return parent ::get_first_child ();
6698 }
6799
68100 /** @inheritDoc */
69101 public function get_first_child_node ( ?string $ rule_name = null ): ?WP_Parser_Node {
70- if ( $ this ->has_native_ast () ) {
102+ if ( $ this ->has_unmaterialized_native_ast () ) {
71103 return wp_sqlite_mysql_native_ast_get_first_child_node ( $ this ->native_ast , $ this ->native_node_index , $ rule_name );
72104 }
73105 return parent ::get_first_child_node ( $ rule_name );
74106 }
75107
76108 /** @inheritDoc */
77109 public function get_first_child_token ( ?int $ token_id = null ): ?WP_Parser_Token {
78- if ( $ this ->has_native_ast () ) {
110+ if ( $ this ->has_unmaterialized_native_ast () ) {
79111 return wp_sqlite_mysql_native_ast_get_first_child_token ( $ this ->native_ast , $ this ->native_node_index , $ token_id );
80112 }
81113 return parent ::get_first_child_token ( $ token_id );
82114 }
83115
84116 /** @inheritDoc */
85117 public function get_first_descendant_node ( ?string $ rule_name = null ): ?WP_Parser_Node {
86- if ( $ this ->has_native_ast () ) {
118+ if ( $ this ->has_unmaterialized_native_ast () ) {
87119 return wp_sqlite_mysql_native_ast_get_first_descendant_node ( $ this ->native_ast , $ this ->native_node_index , $ rule_name );
88120 }
89121 return parent ::get_first_descendant_node ( $ rule_name );
90122 }
91123
92124 /** @inheritDoc */
93125 public function get_first_descendant_token ( ?int $ token_id = null ): ?WP_Parser_Token {
94- if ( $ this ->has_native_ast () ) {
126+ if ( $ this ->has_unmaterialized_native_ast () ) {
95127 return wp_sqlite_mysql_native_ast_get_first_descendant_token ( $ this ->native_ast , $ this ->native_node_index , $ token_id );
96128 }
97129 return parent ::get_first_descendant_token ( $ token_id );
98130 }
99131
100132 /** @inheritDoc */
101133 public function get_children (): array {
102- if ( $ this ->has_native_ast () ) {
134+ if ( $ this ->has_unmaterialized_native_ast () ) {
103135 return wp_sqlite_mysql_native_ast_get_children ( $ this ->native_ast , $ this ->native_node_index );
104136 }
105137 return parent ::get_children ();
106138 }
107139
108140 /** @inheritDoc */
109141 public function get_child_nodes ( ?string $ rule_name = null ): array {
110- if ( $ this ->has_native_ast () ) {
142+ if ( $ this ->has_unmaterialized_native_ast () ) {
111143 return wp_sqlite_mysql_native_ast_get_child_nodes ( $ this ->native_ast , $ this ->native_node_index , $ rule_name );
112144 }
113145 return parent ::get_child_nodes ( $ rule_name );
114146 }
115147
116148 /** @inheritDoc */
117149 public function get_child_tokens ( ?int $ token_id = null ): array {
118- if ( $ this ->has_native_ast () ) {
150+ if ( $ this ->has_unmaterialized_native_ast () ) {
119151 return wp_sqlite_mysql_native_ast_get_child_tokens ( $ this ->native_ast , $ this ->native_node_index , $ token_id );
120152 }
121153 return parent ::get_child_tokens ( $ token_id );
122154 }
123155
124156 /** @inheritDoc */
125157 public function get_descendants (): array {
126- if ( $ this ->has_native_ast () ) {
158+ if ( $ this ->has_unmaterialized_native_ast () ) {
127159 return wp_sqlite_mysql_native_ast_get_descendants ( $ this ->native_ast , $ this ->native_node_index );
128160 }
129161 return parent ::get_descendants ();
130162 }
131163
132164 /** @inheritDoc */
133165 public function get_descendant_nodes ( ?string $ rule_name = null ): array {
134- if ( $ this ->has_native_ast () ) {
166+ if ( $ this ->has_unmaterialized_native_ast () ) {
135167 return wp_sqlite_mysql_native_ast_get_descendant_nodes ( $ this ->native_ast , $ this ->native_node_index , $ rule_name );
136168 }
137169 return parent ::get_descendant_nodes ( $ rule_name );
138170 }
139171
140172 /** @inheritDoc */
141173 public function get_descendant_tokens ( ?int $ token_id = null ): array {
142- if ( $ this ->has_native_ast () ) {
174+ if ( $ this ->has_unmaterialized_native_ast () ) {
143175 return wp_sqlite_mysql_native_ast_get_descendant_tokens ( $ this ->native_ast , $ this ->native_node_index , $ token_id );
144176 }
145177 return parent ::get_descendant_tokens ( $ token_id );
146178 }
147179
148180 /** @inheritDoc */
149181 public function get_start (): int {
150- if ( $ this ->has_native_ast () ) {
182+ if ( $ this ->has_unmaterialized_native_ast () ) {
151183 return wp_sqlite_mysql_native_ast_get_start ( $ this ->native_ast , $ this ->native_node_index );
152184 }
153185 return parent ::get_start ();
154186 }
155187
156188 /** @inheritDoc */
157189 public function get_length (): int {
158- if ( $ this ->has_native_ast () ) {
190+ if ( $ this ->has_unmaterialized_native_ast () ) {
159191 return wp_sqlite_mysql_native_ast_get_length ( $ this ->native_ast , $ this ->native_node_index );
160192 }
161193 return parent ::get_length ();
162194 }
163195
164- private function has_native_ast (): bool {
196+ /**
197+ * Indicates whether this node still has an unmaterialized native AST.
198+ *
199+ * Returns true for freshly-parsed nodes whose children live in the
200+ * Rust-owned AST buffer; returns false once the node has been mutated and
201+ * its children copied into the inherited `$children` array (see
202+ * self::materialize_native_children()).
203+ *
204+ * This is a per-instance state check, not a check for whether the native
205+ * extension is loaded.
206+ */
207+ private function has_unmaterialized_native_ast (): bool {
165208 return null !== $ this ->native_ast ;
166209 }
167210
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+ * has_unmaterialized_native_ast() returns false and read methods fall
218+ * through to the parent WP_Parser_Node implementation.
219+ */
168220 private function materialize_native_children (): void {
169- if ( ! $ this ->has_native_ast () ) {
221+ if ( ! $ this ->has_unmaterialized_native_ast () ) {
170222 return ;
171223 }
172224
0 commit comments