Skip to content

Commit cd22199

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 857fbec commit cd22199

3 files changed

Lines changed: 232 additions & 1 deletion

File tree

packages/mysql-on-sqlite/src/load.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
if ( class_exists( 'WP_MySQL_Native_Parser', false ) ) {
2929
require_once __DIR__ . '/mysql/native/mysql-rust-bridge.php';
30+
require_once __DIR__ . '/mysql/native/class-wp-mysql-native-parser-node.php';
3031
require_once __DIR__ . '/mysql/native/class-wp-mysql-parser.php';
3132
} else {
3233
require_once __DIR__ . '/mysql/class-wp-mysql-parser.php';
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
<?php
2+
3+
/**
4+
* Parser node backed by a native (Rust) AST.
5+
*
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.
29+
*/
30+
class WP_MySQL_Native_Parser_Node extends WP_Parser_Node {
31+
private $native_ast = null;
32+
private $native_node_index = null;
33+
private $was_mutated = false;
34+
35+
public function __construct( $rule_id, $rule_name, $native_ast = null, $native_node_index = null ) {
36+
parent::__construct( $rule_id, $rule_name );
37+
38+
$this->native_ast = $native_ast;
39+
$this->native_node_index = $native_node_index;
40+
}
41+
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+
*/
49+
public function append_child( $node ) {
50+
$this->materialize_native_children();
51+
parent::append_child( $node );
52+
}
53+
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+
*/
60+
public function merge_fragment( $node ) {
61+
$this->materialize_native_children();
62+
if ( $node instanceof self ) {
63+
$node->materialize_native_children();
64+
}
65+
parent::merge_fragment( $node );
66+
}
67+
68+
/** @inheritDoc */
69+
public function has_child(): bool {
70+
if ( $this->was_mutated() ) {
71+
return parent::has_child();
72+
}
73+
return wp_sqlite_mysql_native_ast_has_child( $this->native_ast, $this->native_node_index );
74+
}
75+
76+
/** @inheritDoc */
77+
public function has_child_node( ?string $rule_name = null ): bool {
78+
if ( $this->was_mutated() ) {
79+
return parent::has_child_node( $rule_name );
80+
}
81+
return wp_sqlite_mysql_native_ast_has_child_node( $this->native_ast, $this->native_node_index, $rule_name );
82+
}
83+
84+
/** @inheritDoc */
85+
public function has_child_token( ?int $token_id = null ): bool {
86+
if ( $this->was_mutated() ) {
87+
return parent::has_child_token( $token_id );
88+
}
89+
return wp_sqlite_mysql_native_ast_has_child_token( $this->native_ast, $this->native_node_index, $token_id );
90+
}
91+
92+
/** @inheritDoc */
93+
public function get_first_child() {
94+
if ( $this->was_mutated() ) {
95+
return parent::get_first_child();
96+
}
97+
return wp_sqlite_mysql_native_ast_get_first_child( $this->native_ast, $this->native_node_index );
98+
}
99+
100+
/** @inheritDoc */
101+
public function get_first_child_node( ?string $rule_name = null ): ?WP_Parser_Node {
102+
if ( $this->was_mutated() ) {
103+
return parent::get_first_child_node( $rule_name );
104+
}
105+
return wp_sqlite_mysql_native_ast_get_first_child_node( $this->native_ast, $this->native_node_index, $rule_name );
106+
}
107+
108+
/** @inheritDoc */
109+
public function get_first_child_token( ?int $token_id = null ): ?WP_Parser_Token {
110+
if ( $this->was_mutated() ) {
111+
return parent::get_first_child_token( $token_id );
112+
}
113+
return wp_sqlite_mysql_native_ast_get_first_child_token( $this->native_ast, $this->native_node_index, $token_id );
114+
}
115+
116+
/** @inheritDoc */
117+
public function get_first_descendant_node( ?string $rule_name = null ): ?WP_Parser_Node {
118+
if ( $this->was_mutated() ) {
119+
return parent::get_first_descendant_node( $rule_name );
120+
}
121+
return wp_sqlite_mysql_native_ast_get_first_descendant_node( $this->native_ast, $this->native_node_index, $rule_name );
122+
}
123+
124+
/** @inheritDoc */
125+
public function get_first_descendant_token( ?int $token_id = null ): ?WP_Parser_Token {
126+
if ( $this->was_mutated() ) {
127+
return parent::get_first_descendant_token( $token_id );
128+
}
129+
return wp_sqlite_mysql_native_ast_get_first_descendant_token( $this->native_ast, $this->native_node_index, $token_id );
130+
}
131+
132+
/** @inheritDoc */
133+
public function get_children(): array {
134+
if ( $this->was_mutated() ) {
135+
return parent::get_children();
136+
}
137+
return wp_sqlite_mysql_native_ast_get_children( $this->native_ast, $this->native_node_index );
138+
}
139+
140+
/** @inheritDoc */
141+
public function get_child_nodes( ?string $rule_name = null ): array {
142+
if ( $this->was_mutated() ) {
143+
return parent::get_child_nodes( $rule_name );
144+
}
145+
return wp_sqlite_mysql_native_ast_get_child_nodes( $this->native_ast, $this->native_node_index, $rule_name );
146+
}
147+
148+
/** @inheritDoc */
149+
public function get_child_tokens( ?int $token_id = null ): array {
150+
if ( $this->was_mutated() ) {
151+
return parent::get_child_tokens( $token_id );
152+
}
153+
return wp_sqlite_mysql_native_ast_get_child_tokens( $this->native_ast, $this->native_node_index, $token_id );
154+
}
155+
156+
/** @inheritDoc */
157+
public function get_descendants(): array {
158+
if ( $this->was_mutated() ) {
159+
return parent::get_descendants();
160+
}
161+
return wp_sqlite_mysql_native_ast_get_descendants( $this->native_ast, $this->native_node_index );
162+
}
163+
164+
/** @inheritDoc */
165+
public function get_descendant_nodes( ?string $rule_name = null ): array {
166+
if ( $this->was_mutated() ) {
167+
return parent::get_descendant_nodes( $rule_name );
168+
}
169+
return wp_sqlite_mysql_native_ast_get_descendant_nodes( $this->native_ast, $this->native_node_index, $rule_name );
170+
}
171+
172+
/** @inheritDoc */
173+
public function get_descendant_tokens( ?int $token_id = null ): array {
174+
if ( $this->was_mutated() ) {
175+
return parent::get_descendant_tokens( $token_id );
176+
}
177+
return wp_sqlite_mysql_native_ast_get_descendant_tokens( $this->native_ast, $this->native_node_index, $token_id );
178+
}
179+
180+
/** @inheritDoc */
181+
public function get_start(): int {
182+
if ( $this->was_mutated() ) {
183+
return parent::get_start();
184+
}
185+
return wp_sqlite_mysql_native_ast_get_start( $this->native_ast, $this->native_node_index );
186+
}
187+
188+
/** @inheritDoc */
189+
public function get_length(): int {
190+
if ( $this->was_mutated() ) {
191+
return parent::get_length();
192+
}
193+
return wp_sqlite_mysql_native_ast_get_length( $this->native_ast, $this->native_node_index );
194+
}
195+
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+
*/
220+
private function materialize_native_children(): void {
221+
if ( $this->was_mutated ) {
222+
return;
223+
}
224+
225+
$this->children = wp_sqlite_mysql_native_ast_get_children( $this->native_ast, $this->native_node_index );
226+
$this->native_ast = null;
227+
$this->native_node_index = null;
228+
$this->was_mutated = true;
229+
}
230+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class WP_Parser_Node {
1515
*/
1616
public $rule_id;
1717
public $rule_name;
18-
private $children = array();
18+
protected $children = array();
1919

2020
public function __construct( $rule_id, $rule_name ) {
2121
$this->rule_id = $rule_id;

0 commit comments

Comments
 (0)