This commit is contained in:
zbatte committed 2026-07-02 15:54:39 -06:00
commit 9883323161
17470 files changed
+4470592

No files matched your search

@@ -0,0 +1,63 @@
<?php
class WP_MySQL_Parser extends WP_Parser {
/**
* The current query AST.
*
* @var WP_Parser_Node|null
*/
private $current_ast;
/**
* Reset this parser with a new token stream.
*
* @param array<WP_Parser_Token> $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.
*
* This method reads tokens until a query is parsed, or the parsing fails.
* It returns a boolean indicating whether a query was successfully parsed.
*
* Example:
*
* // Parse all queries in the input SQL string.
* $parser = new WP_MySQL_Parser( $sql );
* while ( $parser->next_query() ) {
* $ast = $parser->get_query_ast();
* if ( ! $ast ) {
* // The parsing failed.
* }
* // The query was successfully parsed.
* }
*
* @return bool Whether a query was successfully parsed.
*/
public function next_query(): bool {
if ( $this->position >= count( $this->tokens ) ) {
return false;
}
$this->current_ast = $this->parse();
return true;
}
/**
* Get the current query AST.
*
* When no query has been parsed yet, the parsing failed, or the end of the
* input was reached, this method returns null.
*
* @see WP_MySQL_Parser::next_query() for usage example.
*
* @return WP_Parser_Node|null The current query AST, or null if no query was parsed.
*/
public function get_query_ast(): ?WP_Parser_Node {
return $this->current_ast;
}
}
@@ -0,0 +1,187 @@
<?php
/**
* MySQL token.
*
* This class represents a MySQL SQL token that is produced by WP_MySQL_Lexer,
* and consumed by WP_MySQL_Parser during the parsing process.
*/
class WP_MySQL_Token extends WP_Parser_Token {
/**
* Whether the NO_BACKSLASH_ESCAPES SQL mode is enabled.
*
* @var bool
*/
private $sql_mode_no_backslash_escapes_enabled;
/**
* Constructor.
*
* @param int $id Token type.
* @param int $start Byte offset in the input where the token begins.
* @param int $length Byte length of the token in the input.
* @param string $input Input bytes from which the token was parsed.
* @param bool $sql_mode_no_backslash_escapes_enabled Whether the NO_BACKSLASH_ESCAPES SQL mode is enabled.
*/
public function __construct(
int $id,
int $start,
int $length,
string $input,
bool $sql_mode_no_backslash_escapes_enabled
) {
$this->id = $id;
$this->start = $start;
$this->length = $length;
$this->input = $input;
$this->sql_mode_no_backslash_escapes_enabled = $sql_mode_no_backslash_escapes_enabled;
}
/**
* Get the name of the token.
*
* This method is intended to be used only for testing and debugging purposes,
* when tokens need to be presented by their names in a human-readable form.
* It should not be used in production code, as it's not performance-optimized.
*
* @return string The token name.
*/
public function get_name(): string {
$name = WP_MySQL_Lexer::get_token_name( $this->id );
if ( null === $name ) {
$name = 'UNKNOWN';
}
return $name;
}
/**
* Get the real unquoted value of the token.
*
* @return string The token value.
*/
public function get_value(): string {
$value = $this->get_bytes();
if (
WP_MySQL_Lexer::SINGLE_QUOTED_TEXT === $this->id
|| WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT === $this->id
|| WP_MySQL_Lexer::BACK_TICK_QUOTED_ID === $this->id
) {
// Remove bounding quotes.
$quote = $value[0];
$value = substr( $value, 1, -1 );
/*
* When the NO_BACKSLASH_ESCAPES SQL mode is enabled, we only need to
* handle escaped bounding quotes, as the other characters preserve
* their literal values.
*/
if ( $this->sql_mode_no_backslash_escapes_enabled ) {
return str_replace( $quote . $quote, $quote, $value );
}
/**
* Unescape MySQL escape sequences.
*
* MySQL string literals use backslash as an escape character, and
* the string bounding quotes can also be escaped by being doubled.
*
* The escaping is done according to the following rules:
*
* 1. Some special character escape sequences are recognized.
* For example, "\n" is a newline character, "\0" is ASCII NULL.
* 2. A specific treatment is applied to "\%" and "\_" sequences.
* This is due to their special meaning for pattern matching.
* 3. Other backslash-prefixed characters resolve to their literal
* values. For example, "\x" represents "x", "\\" represents "\".
*
* Despite looking similar, these rules are different from the C-style
* string escaping, so we cannot use "strip(c)slashes()" in this case.
*
* See: https://dev.mysql.com/doc/refman/8.4/en/string-literals.html
*/
$backslash = chr( 92 );
$replacements = array(
/*
* MySQL special character escape sequences.
*/
( $backslash . '0' ) => chr( 0 ), // An ASCII NULL character (\0).
( $backslash . "'" ) => chr( 39 ), // A single quote character (').
( $backslash . '"' ) => chr( 34 ), // A double quote character (").
( $backslash . 'b' ) => chr( 8 ), // A backspace character.
( $backslash . 'n' ) => chr( 10 ), // A newline (linefeed) character (\n).
( $backslash . 'r' ) => chr( 13 ), // A carriage return character (\r).
( $backslash . 't' ) => chr( 9 ), // A tab character (\t).
( $backslash . 'Z' ) => chr( 26 ), // An ASCII 26 (Control+Z) character.
/*
* Normalize escaping of "%" and "_" characters.
*
* MySQL has unusual handling for "\%" and "\_" in all string literals.
* While other sequences follow the C-style escaping ("\?" is "?", etc.),
* "\%" resolves to "\%" and "\_" resolves to "\_" (unlike in C strings).
*
* This means that "\%" behaves like "\\%", and "\_" behaves like "\\_".
* To preserve this behavior, we need to add a second backslash here.
*
* From https://dev.mysql.com/doc/refman/8.4/en/string-literals.html:
* > The \% and \_ sequences are used to search for literal instances
* > of % and _ in pattern-matching contexts where they would otherwise
* > be interpreted as wildcard characters. If you use \% or \_ outside
* > of pattern-matching contexts, they evaluate to the strings \% and
* > \_, not to % and _.
*/
( $backslash . '%' ) => $backslash . $backslash . '%',
( $backslash . '_' ) => $backslash . $backslash . '_',
/*
* Preserve a double backslash as-is, so that the trailing backslash
* is not consumed as the beginning of an escape sequence like "\n".
*
* Resolving "\\" to "\" will be handled in the next step, where all
* other backslash-prefixed characters resolve to their literal values.
*/
( $backslash . $backslash )
=> $backslash . $backslash,
/*
* The bounding quotes can also be escaped by being doubled.
*/
( $quote . $quote ) => $quote,
);
/*
* Apply the replacements.
*
* It is important to use "strtr()" and not "str_replace()", because
* "str_replace()" applies replacements one after another, modifying
* intermediate changes rather than just the original string:
*
* - str_replace( [ 'a', 'b' ], [ 'b', 'c' ], 'ab' ); // 'cc' (bad)
* - strtr( 'ab', [ 'a' => 'b', 'b' => 'c' ] ); // 'bc' (good)
*/
$value = strtr( $value, $replacements );
/*
* A backslash with any other character represents the character itself.
* That is, \x evaluates to x, \\ evaluates to \, and \🙂 evaluates to 🙂.
*/
$preg_quoted_backslash = preg_quote( $backslash );
$value = preg_replace( "/$preg_quoted_backslash(.)/u", '$1', $value );
}
return $value;
}
/**
* Get the token representation as a string.
*
* This method is intended to be used only for testing and debugging purposes,
* when tokens need to be presented in a human-readable form. It should not
* be used in production code, as it's not performance-optimized.
*
* @return string
*/
public function __toString(): string {
return $this->get_value() . '<' . $this->id . ',' . $this->get_name() . '>';
}
}
@@ -0,0 +1,4 @@
<?php
// THIS FILE IS GENERATED. DO NOT MODIFY IT MANUALLY.
// phpcs:disable
return ['rules_offset'=>2000,'rules_names'=>['query','%f1','%f2','%f3','simpleStatement','%f4','%f5','%f6','%f7','alterStatement','%f8','%f9','%f10','alterInstance','%f11','%f12','%f13','%f14','%f15','%f16','%f17','%f18','%f19','%f20','%f21','%f22','%f23','%f24','alterDatabase','%f25','%f26','%f27','%f28','%f29','%f30','%f31','%f32','%f33','%f34','alterEvent','%f35','%f36','%f37','%f38','%f39','%f40','%f41','%f42','%f43','%f44','alterLogfileGroup','%f45','alterLogfileGroupOptions','%f46','%f47','alterLogfileGroupOption','alterServer','%f48','%f49','%f50','alterTable','%f51','%f52','%f53','%f54','alterTableActions','%f55','%f56','%f57','alterCommandList','%f58','%f59','alterCommandsModifierList','%f60','standaloneAlterCommands','%f61','%f62','%f63','%f64','%f65','%f66','%f67','alterPartition','%f68','%f69','%f70','%f71','%f72','alterList','%f73','%f74','%f75','alterCommandsModifier','%f76','%f77','%f78','%f79','alterListItem','%f80','%f81','%f82','%f83','%f84','%f85','%f86','%f87','%f88','%f89','%f90','%f91','%f92','%f93','%f94','%f95','%f96','%f97','%f98','%f99','%f100','%f101','%f102','place','restrict','%f103','%f104','alterOrderList','%f105','%f106','alterAlgorithmOption','%f107','alterLockOption','%f108','%f109','%f110','indexLockAndAlgorithm','withValidation','%f111','%f112','removePartitioning','allOrPartitionNameList','alterTablespace','%f113','%f114','%f115','%f116','%f117','%f118','%f119','%f120','%f121','%f122','%f123','%f124','alterUndoTablespace','%f125','%f126','undoTableSpaceOptions','%f127','undoTableSpaceOption','%f128','alterTablespaceOptions','%f129','alterTablespaceOption','%f130','changeTablespaceOption','%f131','%f132','alterView','%f133','viewTail','%f134','viewSelect','%f135','viewCheckOption','%f136','createStatement','%f137','%f138','%f139','%f140','%f141','%f142','createDatabase','createDatabaseOption','%f143','%f144','createTable','%f145','%f146','%f147','%f148','%f149','%f150','%f151','tableElementList','%f152','tableElement','%f153','%f154','duplicateAsQueryExpression','%f155','queryExpressionOrParens','%f156','createRoutine','%f157','%f158','%f159','createProcedure','%f160','%f161','%f162','%f163','%f164','%f165','createFunction','%f166','%f167','%f168','%f169','%f170','%f171','createUdf','%f172','routineCreateOption','%f173','routineAlterOptions','routineOption','%f174','%f175','createIndex','%f176','%f177','%f178','%f179','%f180','%f181','%f182','%f183','%f184','%f185','%f186','indexNameAndType','%f187','%f188','createIndexTarget','%f189','createLogfileGroup','%f190','%f191','logfileGroupOptions','%f192','logfileGroupOption','createServer','%f193','serverOptions','%f194','serverOption','%f195','%f196','createTablespace','%f197','createUndoTablespace','tsDataFileName','%f198','%f199','%f200','tsDataFile','%f201','tablespaceOptions','%f202','tablespaceOption','%f203','%f204','%f205','tsOptionInitialSize','tsOptionUndoRedoBufferSize','%f206','tsOptionAutoextendSize','tsOptionMaxSize','tsOptionExtentSize','tsOptionNodegroup','%f207','tsOptionEngine','tsOptionEngineAttribute','tsOptionWait','%f208','tsOptionComment','tsOptionFileblockSize','tsOptionEncryption','%f209','createView','viewReplaceOrAlgorithm','viewAlgorithm','%f210','viewSuid','%f211','%f212','createTrigger','%f213','%f214','%f215','%f216','triggerFollowsPrecedesClause','%f217','%f218','%f219','%f220','%f221','createEvent','%f222','%f223','%f224','%f225','%f226','createRole','%f227','createSpatialReference','srsAttribute','dropStatement','%f228','%f229','%f230','%f231','%f232','dropDatabase','dropEvent','dropFunction','dropProcedure','dropIndex','%f233','dropLogfileGroup','%f234','%f235','%f236','dropLogfileGroupOption','dropServer','%f237','dropTable','%f238','%f239','%f240','dropTableSpace','%f241','%f242','%f243','dropTrigger','%f244','dropView','%f245','dropRole','dropSpatialReference','dropUndoTablespace','%f246','renameTableStatement','%f247','%f248','renamePair','%f249','truncateTableStatement','importStatement','%f250','callStatement','%f251','%f252','%f253','%f254','deleteStatement','%f255','%f256','%f257','%f258','%f259','%f260','%f261','%f262','%f263','%f264','%f265','partitionDelete','%f266','deleteStatementOption','doStatement','%f267','%f268','%f269','handlerStatement','%f270','%f271','%f272','%f273','handlerReadOrScan','%f274','%f275','%f276','%f277','%f278','%f279','%f280','%f281','%f282','insertStatement','%f283','%f284','%f285','%f286','%f287','%f288','%f289','insertLockOption','%f290','insertFromConstructor','%f291','%f292','%f293','fields','%f294','insertValues','%f295','insertQueryExpression','%f296','%f297','valueList','%f298','%f299','values','%f300','%f301','%f302','valuesReference','insertUpdateList','%f303','%f304','%f305','%f306','%f307','%f308','%f309','loadStatement','%f310','%f311','dataOrXml','xmlRowsIdentifiedBy','%f312','%f313','%f314','loadDataFileTail','%f315','%f316','%f317','%f318','loadDataFileTargetList','%f319','fieldOrVariableList','%f320','%f321','%f322','%f323','replaceStateLine truncated
@@ -0,0 +1,3 @@
<?php
class WP_MySQL_Lexer extends WP_MySQL_Native_Lexer {}
@@ -0,0 +1,179 @@
<?php
/**
* 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.
*/
class WP_MySQL_Native_Parser_Node extends WP_Parser_Node {
private $was_mutated = false;
public function __construct( $rule_id, $rule_name ) {
parent::__construct( $rule_id, $rule_name );
}
public function __destruct() {
if ( function_exists( 'wp_sqlite_mysql_native_ast_release_wrapper' ) ) {
wp_sqlite_mysql_native_ast_release_wrapper( $this );
}
}
/** @inheritDoc */
public function append_child( $node ) {
$this->materialize_native_children();
parent::append_child( $node );
}
/** @inheritDoc */
public function merge_fragment( $node ) {
$this->materialize_native_children();
if ( $node instanceof self ) {
$node->materialize_native_children();
}
parent::merge_fragment( $node );
}
/** @inheritDoc */
public function has_child(): bool {
if ( $this->was_mutated ) {
return parent::has_child();
}
return wp_sqlite_mysql_native_ast_has_child( $this );
}
/** @inheritDoc */
public function has_child_node( ?string $rule_name = null ): bool {
if ( $this->was_mutated ) {
return parent::has_child_node( $rule_name );
}
return wp_sqlite_mysql_native_ast_has_child_node( $this, $rule_name );
}
/** @inheritDoc */
public function has_child_token( ?int $token_id = null ): bool {
if ( $this->was_mutated ) {
return parent::has_child_token( $token_id );
}
return wp_sqlite_mysql_native_ast_has_child_token( $this, $token_id );
}
/** @inheritDoc */
public function get_first_child() {
if ( $this->was_mutated ) {
return parent::get_first_child();
}
return wp_sqlite_mysql_native_ast_get_first_child( $this );
}
/** @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 );
}
return wp_sqlite_mysql_native_ast_get_first_child_node( $this, $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 );
}
return wp_sqlite_mysql_native_ast_get_first_child_token( $this, $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 );
}
return wp_sqlite_mysql_native_ast_get_first_descendant_node( $this, $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 );
}
return wp_sqlite_mysql_native_ast_get_first_descendant_token( $this, $token_id );
}
/** @inheritDoc */
public function get_children(): array {
if ( $this->was_mutated ) {
return parent::get_children();
}
return wp_sqlite_mysql_native_ast_get_children( $this );
}
/** @inheritDoc */
public function get_child_nodes( ?string $rule_name = null ): array {
if ( $this->was_mutated ) {
return parent::get_child_nodes( $rule_name );
}
return wp_sqlite_mysql_native_ast_get_child_nodes( $this, $rule_name );
}
/** @inheritDoc */
public function get_child_tokens( ?int $token_id = null ): array {
if ( $this->was_mutated ) {
return parent::get_child_tokens( $token_id );
}
return wp_sqlite_mysql_native_ast_get_child_tokens( $this, $token_id );
}
/** @inheritDoc */
public function get_descendants(): array {
if ( $this->was_mutated ) {
return parent::get_descendants();
}
return wp_sqlite_mysql_native_ast_get_descendants( $this );
}
/** @inheritDoc */
public function get_descendant_nodes( ?string $rule_name = null ): array {
if ( $this->was_mutated ) {
return parent::get_descendant_nodes( $rule_name );
}
return wp_sqlite_mysql_native_ast_get_descendant_nodes( $this, $rule_name );
}
/** @inheritDoc */
public function get_descendant_tokens( ?int $token_id = null ): array {
if ( $this->was_mutated ) {
return parent::get_descendant_tokens( $token_id );
}
return wp_sqlite_mysql_native_ast_get_descendant_tokens( $this, $token_id );
}
/** @inheritDoc */
public function get_start(): int {
if ( $this->was_mutated ) {
return parent::get_start();
}
return wp_sqlite_mysql_native_ast_get_start( $this );
}
/** @inheritDoc */
public function get_length(): int {
if ( $this->was_mutated ) {
return parent::get_length();
}
return wp_sqlite_mysql_native_ast_get_length( $this );
}
private function materialize_native_children(): void {
if ( $this->was_mutated ) {
return;
}
$this->children = wp_sqlite_mysql_native_ast_get_children( $this );
$this->was_mutated = true;
if ( function_exists( 'wp_sqlite_mysql_native_ast_materialize_wrapper' ) ) {
wp_sqlite_mysql_native_ast_materialize_wrapper( $this );
}
}
}
@@ -0,0 +1,14 @@
<?php
/**
* Native-mode public parser entry point.
*
* Always extends the pure-PHP `WP_Parser` so `$parser instanceof WP_Parser`
* keeps working for callers regardless of whether the Rust extension is
* loaded. The actual parsing work is delegated to a composed
* `WP_MySQL_Native_Parser` instance via the `WP_MySQL_Native_Parser_Impl`
* trait — see that file for the per-method delegation.
*/
class WP_MySQL_Parser extends WP_Parser {
use WP_MySQL_Native_Parser_Impl;
}
@@ -0,0 +1,22 @@
<?php
/**
* Bridge helpers for the optional Rust MySQL lexer/parser extension.
* PHP keeps the grammar object, while Rust owns the exported parser state.
*/
/**
* Export grammar internals for the native parser.
*
* @param WP_Parser_Grammar $grammar Parser grammar.
* @return array<string, mixed>
*/
function wp_sqlite_mysql_native_export_grammar( WP_Parser_Grammar $grammar ): array {
return array(
'highest_terminal_id' => $grammar->highest_terminal_id,
'rules' => $grammar->rules,
'lookahead_is_match_possible' => $grammar->lookahead_is_match_possible,
'rule_names' => $grammar->rule_names,
'fragment_ids' => $grammar->fragment_ids,
);
}
@@ -0,0 +1,55 @@
<?php
/**
* Native-mode `WP_MySQL_Parser` implementation, delivered as a trait.
*
* The class that uses this trait (`WP_MySQL_Parser` in native mode)
* extends the pure-PHP `WP_Parser` so callers' `instanceof WP_Parser`
* checks keep working, while the actual parsing work is delegated to
* the Rust-registered `WP_MySQL_Native_Parser` instance held in
* `$this->native`. `WP_Parser`'s state (`$grammar`, `$tokens`,
* `$position`) stays inert in native mode — the trait's overrides
* never read it.
*
* Adding a public method here is enough to plumb a new public method
* through to the native parser; the using class does not need touching.
*/
trait WP_MySQL_Native_Parser_Impl {
/**
* @var WP_MySQL_Native_Parser
*/
private $native;
/**
* @param WP_Parser_Grammar $grammar
* @param array<WP_Parser_Token>|WP_MySQL_Native_Token_Stream $tokens
*/
public function __construct( WP_Parser_Grammar $grammar, $tokens ) {
// WP_Parser's `array $tokens` constructor signature can't accept
// the native token stream object; its `$this->tokens` /
// `$this->position` state is inert in native mode anyway, so we
// pass an empty array to satisfy the parent contract and keep
// the actual tokens on the native parser.
parent::__construct( $grammar, array() );
$this->native = new WP_MySQL_Native_Parser( $grammar, $tokens );
}
/**
* @param array<WP_Parser_Token>|WP_MySQL_Native_Token_Stream $tokens
*/
public function reset_tokens( $tokens ): void {
$this->native->reset_tokens( $tokens );
}
public function next_query(): bool {
return $this->native->next_query();
}
public function get_query_ast(): ?WP_Parser_Node {
return $this->native->get_query_ast();
}
public function parse() {
return $this->native->parse();
}
}