initial
This commit is contained in:
+3070
File diff suppressed because it is too large
Load Diff
+63
@@ -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;
|
||||
}
|
||||
}
|
||||
+187
@@ -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() . '>';
|
||||
}
|
||||
}
|
||||
+4
File diff suppressed because one or more lines are too long
+3
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
class WP_MySQL_Lexer extends WP_MySQL_Native_Lexer {}
|
||||
+179
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -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;
|
||||
}
|
||||
+22
@@ -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,
|
||||
);
|
||||
}
|
||||
+55
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user