Skip to content

Commit e815c2b

Browse files
committed
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.
1 parent 8c5eb3f commit e815c2b

1 file changed

Lines changed: 93 additions & 42 deletions

File tree

packages/mysql-on-sqlite/src/mysql/native/class-wp-mysql-native-parser-node.php

Lines changed: 93 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,29 @@
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
*/
1030
class 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

Comments
 (0)