From aac00ceee4e00c66ecd5875589508f7160e05c50 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 13 Nov 2024 12:46:25 +0100
Subject: [PATCH 001/124] Split, cleanup, and reorganize the SQLite driver
prototype
---
.gitattributes | 2 +-
tests/tools/dump-sqlite-query.php | 21 ++
wp-includes/mysql/class-wp-mysql-token.php | 4 +
.../sqlite-ast/class-wp-sqlite-driver.php | 353 ++++++++----------
.../sqlite-ast/class-wp-sqlite-expression.php | 36 ++
.../class-wp-sqlite-query-builder.php | 38 ++
.../class-wp-sqlite-token-factory.php | 178 +--------
.../sqlite-ast/class-wp-sqlite-token.php | 16 +
8 files changed, 290 insertions(+), 358 deletions(-)
create mode 100644 tests/tools/dump-sqlite-query.php
rename wip/run-mysql-driver.php => wp-includes/sqlite-ast/class-wp-sqlite-driver.php (51%)
create mode 100644 wp-includes/sqlite-ast/class-wp-sqlite-expression.php
create mode 100644 wp-includes/sqlite-ast/class-wp-sqlite-query-builder.php
rename wip/SQLiteDriver.php => wp-includes/sqlite-ast/class-wp-sqlite-token-factory.php (64%)
create mode 100644 wp-includes/sqlite-ast/class-wp-sqlite-token.php
diff --git a/.gitattributes b/.gitattributes
index e1ddbbb..5a8431e 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -7,7 +7,7 @@ phpcs.xml.dist export-ignore
phpunit.xml.dist export-ignore
/grammar-tools export-ignore
/tests export-ignore
-/wip export-ignore
/wp-includes/mysql export-ignore
/wp-includes/parser export-ignore
+/wp-includes/sqlite-ast export-ignore
wp-includes/sqlite/class-wp-sqlite-crosscheck-db.php export-ignore
diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php
new file mode 100644
index 0000000..4cf4fdc
--- /dev/null
+++ b/tests/tools/dump-sqlite-query.php
@@ -0,0 +1,21 @@
+run_query( $query );
diff --git a/wp-includes/mysql/class-wp-mysql-token.php b/wp-includes/mysql/class-wp-mysql-token.php
index 5251264..ba112a7 100644
--- a/wp-includes/mysql/class-wp-mysql-token.php
+++ b/wp-includes/mysql/class-wp-mysql-token.php
@@ -29,6 +29,10 @@ public function get_name() {
return WP_MySQL_Lexer::get_token_name( $this->type );
}
+ public function extract_value() {
+ return $this->get_text();
+ }
+
public function __toString() {
return $this->text . '<' . $this->type . ',' . $this->get_name() . '>';
}
diff --git a/wip/run-mysql-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
similarity index 51%
rename from wip/run-mysql-driver.php
rename to wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 7f257d7..9f53f91 100644
--- a/wip/run-mysql-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1,54 +1,7 @@
-parse();
-// print_r($parse_tree);
-// die();
-// echo 'a';
-
-$query = <<run_query( $query );
-die();
-// $transformer = new SQLTransformer($parse_tree, 'sqlite');
-// $expression = $transformer->transform();
-// print_r($expression);
-
-class MySQLonSQLiteDriver {
- private $grammar = false;
+has_found_rows_call = false;
$this->last_calc_rows_result = null;
- $parser = new WP_MySQL_Parser( $this->grammar, tokenize_query( $query ) );
- $parse_tree = $parser->parse();
- $expr = $this->translate_query( $parse_tree );
- $expr = $this->rewrite_sql_calc_found_rows( $expr );
+ $lexer = new WP_MySQL_Lexer( $query );
+ $tokens = $lexer->remaining_tokens();
+
+ $parser = new WP_MySQL_Parser( $this->grammar, $tokens );
+ $ast = $parser->parse();
+ $expr = $this->translate_query( $ast );
+ $expr = $this->rewrite_sql_calc_found_rows( $expr );
- $sqlite_query = SQLiteQueryBuilder::stringify( $expr ) . '';
+ $sqlite_query = WP_SQLite_Query_Builder::stringify( $expr );
- // Returning the expery just for now for testing. In the end, we'll
+ // Returning the query just for now for testing. In the end, we'll
// run it and return the SQLite interaction result.
return $sqlite_query;
}
- private function rewrite_sql_calc_found_rows( SQLiteExpression $expr ) {
+ private function rewrite_sql_calc_found_rows( WP_SQLite_Expression $expr ) {
if ( $this->has_found_rows_call && ! $this->has_sql_calc_found_rows && null === $this->last_calc_rows_result ) {
throw new Exception( 'FOUND_ROWS() called without SQL_CALC_FOUND_ROWS' );
}
@@ -82,11 +38,11 @@ private function rewrite_sql_calc_found_rows( SQLiteExpression $expr ) {
if ( $this->has_sql_calc_found_rows ) {
$expr_to_run = $expr;
if ( $this->has_found_rows_call ) {
- $expr_without_found_rows = new SQLiteExpression( array() );
+ $expr_without_found_rows = new WP_SQLite_Expression( array() );
foreach ( $expr->elements as $k => $element ) {
- if ( SQLiteToken::TYPE_IDENTIFIER === $element->type && 'FOUND_ROWS' === $element->value ) {
+ if ( WP_SQLite_Token::TYPE_IDENTIFIER === $element->type && 'FOUND_ROWS' === $element->value ) {
$expr_without_found_rows->add_token(
- SQLiteTokenFactory::value( 0 )
+ WP_SQLite_Token_Factory::value( 0 )
);
} else {
$expr_without_found_rows->add_token( $element );
@@ -96,23 +52,22 @@ private function rewrite_sql_calc_found_rows( SQLiteExpression $expr ) {
}
// ...remove the LIMIT clause...
- $query = 'SELECT COUNT(*) as cnt FROM (' . SQLiteQueryBuilder::stringify( $expr_to_run ) . ');';
+ $query = 'SELECT COUNT(*) as cnt FROM (' . WP_SQLite_Query_Builder::stringify( $expr_to_run ) . ');';
// ...run $query...
// $result = ...
-
- $this->last_calc_rows_result = $result['cnt'];
+ // $this->last_calc_rows_result = $result['cnt'];
}
if ( ! $this->has_found_rows_call ) {
return $expr;
}
- $expr_with_found_rows_result = new SQLiteExpression( array() );
+ $expr_with_found_rows_result = new WP_SQLite_Expression( array() );
foreach ( $expr->elements as $k => $element ) {
- if ( SQLiteToken::TYPE_IDENTIFIER === $element->type && 'FOUND_ROWS' === $element->value ) {
+ if ( WP_SQLite_Token::TYPE_IDENTIFIER === $element->type && 'FOUND_ROWS' === $element->value ) {
$expr_with_found_rows_result->add_token(
- SQLiteTokenFactory::value( $this->last_calc_rows_result )
+ WP_SQLite_Token_Factory::value( $this->last_calc_rows_result )
);
} else {
$expr_with_found_rows_result->add_token( $element );
@@ -121,55 +76,54 @@ private function rewrite_sql_calc_found_rows( SQLiteExpression $expr ) {
return $expr_with_found_rows_result;
}
- private function translate_query( $parse_tree ) {
- if ( null === $parse_tree ) {
+ private function translate_query( $ast ) {
+ if ( null === $ast ) {
return null;
}
- if ( $parse_tree instanceof WP_MySQL_Token ) {
- $token = $parse_tree;
+ if ( $ast instanceof WP_MySQL_Token ) {
+ $token = $ast;
switch ( $token->type ) {
case WP_MySQL_Lexer::EOF:
- return new SQLiteExpression( array() );
+ return new WP_SQLite_Expression( array() );
case WP_MySQL_Lexer::IDENTIFIER:
- return new SQLiteExpression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::identifier(
+ WP_SQLite_Token_Factory::identifier(
trim( $token->text, '`"' )
),
)
);
default:
- return new SQLiteExpression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( $token->text ),
+ WP_SQLite_Token_Factory::raw( $token->text ),
)
);
}
}
- if ( ! ( $parse_tree instanceof WP_Parser_Node ) ) {
- throw new Exception( 'translateQuery only accepts MySQLToken and ParseTree instances' );
+ if ( ! ( $ast instanceof WP_Parser_Node ) ) {
+ throw new Exception( 'translate_query only accepts WP_MySQL_Token and WP_Parser_Node instances' );
}
- $rule_name = $parse_tree->rule_name;
+ $rule_name = $ast->rule_name;
switch ( $rule_name ) {
case 'indexHintList':
- // SQLite doesn't support index hints. Let's
- // skip them.
+ // SQLite doesn't support index hints. Let's skip them.
return null;
case 'querySpecOption':
- $token = $parse_tree->get_token();
+ $token = $ast->get_token();
switch ( $token->type ) {
case WP_MySQL_Lexer::ALL_SYMBOL:
case WP_MySQL_Lexer::DISTINCT_SYMBOL:
- return new SQLiteExpression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( $token->text ),
+ WP_SQLite_Token_Factory::raw( $token->text ),
)
);
case WP_MySQL_Lexer::SQL_CALC_FOUND_ROWS_SYMBOL:
@@ -179,7 +133,7 @@ private function translate_query( $parse_tree ) {
// we'll need to run the current SQL query without any
// LIMIT clause, and then substitute the FOUND_ROWS()
// function with a literal number of rows found.
- return new SQLiteExpression( array() );
+ return new WP_SQLite_Expression( array() );
}
// Otherwise, fall through.
@@ -188,8 +142,8 @@ private function translate_query( $parse_tree ) {
// FROM DUAL statement, as FROM mytable, DUAL is a syntax
// error.
if (
- $parse_tree->has_token( WP_MySQL_Lexer::DUAL_SYMBOL ) &&
- ! $parse_tree->has_child( 'tableReferenceList' )
+ $ast->has_token( WP_MySQL_Lexer::DUAL_SYMBOL ) &&
+ ! $ast->has_child( 'tableReferenceList' )
) {
return null;
}
@@ -228,6 +182,7 @@ private function translate_query( $parse_tree ) {
case 'queryExpressionParens':
case 'queryPrimary':
case 'querySpecification':
+ case 'queryTerm':
case 'selectAlias':
case 'selectItem':
case 'selectItemList':
@@ -264,26 +219,26 @@ private function translate_query( $parse_tree ) {
case 'direction':
case 'orderExpression':
$child_expressions = array();
- foreach ( $parse_tree->children as $child ) {
+ foreach ( $ast->children as $child ) {
$child_expressions[] = $this->translate_query( $child );
}
- return new SQLiteExpression( $child_expressions );
+ return new WP_SQLite_Expression( $child_expressions );
case 'textStringLiteral':
- return new SQLiteExpression(
+ return new WP_SQLite_Expression(
array(
- $parse_tree->has_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT ) ?
- SQLiteTokenFactory::double_quoted_value( $parse_tree->get_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT )->text ) : false,
- $parse_tree->has_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ) ?
- SQLiteTokenFactory::raw( $parse_tree->get_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT )->text ) : false,
+ $ast->has_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT ) ?
+ WP_SQLite_Token_Factory::double_quoted_value( $ast->get_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT )->text ) : false,
+ $ast->has_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ) ?
+ WP_SQLite_Token_Factory::raw( $ast->get_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT )->text ) : false,
)
);
case 'functionCall':
- return $this->translate_function_call( $parse_tree );
+ return $this->translate_function_call( $ast );
case 'runtimeFunctionCall':
- return $this->translate_runtime_function_call( $parse_tree );
+ return $this->translate_runtime_function_call( $ast );
default:
// var_dump(count($ast->children));
@@ -292,9 +247,9 @@ private function translate_query( $parse_tree ) {
// echo $child->getText();
// echo "\n\n";
// }
- return new SQLiteExpression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw(
+ WP_SQLite_Token_Factory::raw(
$rule_name
),
)
@@ -302,33 +257,33 @@ private function translate_query( $parse_tree ) {
}
}
- private function translate_runtime_function_call( $parse_tree ): SQLiteExpression {
- $name_token = $parse_tree->children[0];
+ private function translate_runtime_function_call( $ast ): WP_SQLite_Expression {
+ $name_token = $ast->children[0];
switch ( strtoupper( $name_token->text ) ) {
case 'ADDDATE':
case 'DATE_ADD':
- $args = $parse_tree->get_children( 'expr' );
- $interval = $parse_tree->get_child( 'interval' );
+ $args = $ast->get_children( 'expr' );
+ $interval = $ast->get_child( 'interval' );
$timespan = $interval->get_child( 'intervalTimeStamp' )->get_token()->text;
- return SQLiteTokenFactory::create_function(
+ return WP_SQLite_Token_Factory::create_function(
'DATETIME',
array(
$this->translate_query( $args[0] ),
- new SQLiteExpression(
+ new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::value( '+' ),
- SQLiteTokenFactory::raw( '||' ),
+ WP_SQLite_Token_Factory::value( '+' ),
+ WP_SQLite_Token_Factory::raw( '||' ),
$this->translate_query( $args[1] ),
- SQLiteTokenFactory::raw( '||' ),
- SQLiteTokenFactory::value( $timespan ),
+ WP_SQLite_Token_Factory::raw( '||' ),
+ WP_SQLite_Token_Factory::value( $timespan ),
)
),
)
);
case 'DATE_SUB':
- // return new Expression([
+ // return new WP_SQLite_Expression([
// SQLiteTokenFactory::raw("DATETIME("),
// $args[0],
// SQLiteTokenFactory::raw(", '-'"),
@@ -337,16 +292,16 @@ private function translate_runtime_function_call( $parse_tree ): SQLiteExpressio
// ]);
case 'VALUES':
- $column = $parse_tree->get_child()->get_descendant( 'pureIdentifier' );
+ $column = $ast->get_child()->get_descendant( 'pureIdentifier' );
if ( ! $column ) {
throw new Exception( 'VALUES() calls without explicit column names are unsupported' );
}
- $colname = $column->get_token()->extractValue();
- return new SQLiteExpression(
+ $colname = $column->get_token()->extract_value();
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( 'excluded.' ),
- SQLiteTokenFactory::identifier( $colname ),
+ WP_SQLite_Token_Factory::raw( 'excluded.' ),
+ WP_SQLite_Token_Factory::identifier( $colname ),
)
);
default:
@@ -354,7 +309,7 @@ private function translate_runtime_function_call( $parse_tree ): SQLiteExpressio
}
}
- private function translate_function_call( $function_call_tree ): SQLiteExpression {
+ private function translate_function_call( $function_call_tree ): WP_SQLite_Expression {
$name = $function_call_tree->get_child( 'pureIdentifier' )->get_token()->text;
$args = array();
foreach ( $function_call_tree->get_child( 'udfExprList' )->get_children() as $node ) {
@@ -383,27 +338,27 @@ private function translate_function_call( $function_call_tree ): SQLiteExpressio
case 'PI':
case 'LTRIM':
case 'RTRIM':
- return SQLiteTokenFactory::create_function( $name, $args );
+ return WP_SQLite_Token_Factory::create_function( $name, $args );
case 'CEIL':
case 'CEILING':
- return SQLiteTokenFactory::create_function( 'CEIL', $args );
+ return WP_SQLite_Token_Factory::create_function( 'CEIL', $args );
case 'COT':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( '1 / ' ),
- SQLiteTokenFactory::create_function( 'TAN', $args ),
+ WP_SQLite_Token_Factory::raw( '1 / ' ),
+ WP_SQLite_Token_Factory::create_function( 'TAN', $args ),
)
);
case 'LN':
case 'LOG':
case 'LOG2':
- return SQLiteTokenFactory::create_function( 'LOG', $args );
+ return WP_SQLite_Token_Factory::create_function( 'LOG', $args );
case 'LOG10':
- return SQLiteTokenFactory::create_function( 'LOG10', $args );
+ return WP_SQLite_Token_Factory::create_function( 'LOG10', $args );
// case 'MOD':
// return $this->transformBinaryOperation([
@@ -414,26 +369,26 @@ private function translate_function_call( $function_call_tree ): SQLiteExpressio
case 'POW':
case 'POWER':
- return SQLiteTokenFactory::create_function( 'POW', $args );
+ return WP_SQLite_Token_Factory::create_function( 'POW', $args );
// String functions
case 'ASCII':
- return SQLiteTokenFactory::create_function( 'UNICODE', $args );
+ return WP_SQLite_Token_Factory::create_function( 'UNICODE', $args );
case 'CHAR_LENGTH':
case 'LENGTH':
- return SQLiteTokenFactory::create_function( 'LENGTH', $args );
+ return WP_SQLite_Token_Factory::create_function( 'LENGTH', $args );
case 'CONCAT':
- $concated = array( SQLiteTokenFactory::raw( '(' ) );
+ $concated = array( WP_SQLite_Token_Factory::raw( '(' ) );
foreach ( $args as $k => $arg ) {
$concated[] = $arg;
if ( $k < count( $args ) - 1 ) {
- $concated[] = SQLiteTokenFactory::raw( '||' );
+ $concated[] = WP_SQLite_Token_Factory::raw( '||' );
}
}
- $concated[] = SQLiteTokenFactory::raw( ')' );
- return new SQLiteExpression( $concated );
+ $concated[] = WP_SQLite_Token_Factory::raw( ')' );
+ return new WP_SQLite_Expression( $concated );
// case 'CONCAT_WS':
- // return new Expression([
+ // return new WP_SQLite_Expression([
// SQLiteTokenFactory::raw("REPLACE("),
// implode(" || ", array_slice($args, 1)),
// SQLiteTokenFactory::raw(", '', "),
@@ -441,12 +396,12 @@ private function translate_function_call( $function_call_tree ): SQLiteExpressio
// SQLiteTokenFactory::raw(")")
// ]);
case 'INSTR':
- return SQLiteTokenFactory::create_function( 'INSTR', $args );
+ return WP_SQLite_Token_Factory::create_function( 'INSTR', $args );
case 'LCASE':
case 'LOWER':
- return SQLiteTokenFactory::create_function( 'LOWER', $args );
+ return WP_SQLite_Token_Factory::create_function( 'LOWER', $args );
case 'LEFT':
- return SQLiteTokenFactory::create_function(
+ return WP_SQLite_Token_Factory::create_function(
'SUBSTR',
array(
$args[0],
@@ -455,7 +410,7 @@ private function translate_function_call( $function_call_tree ): SQLiteExpressio
)
);
case 'LOCATE':
- return SQLiteTokenFactory::create_function(
+ return WP_SQLite_Token_Factory::create_function(
'INSTR',
array(
$args[1],
@@ -463,44 +418,44 @@ private function translate_function_call( $function_call_tree ): SQLiteExpressio
)
);
case 'REPEAT':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( "REPLACE(CHAR(32), ' ', " ),
+ WP_SQLite_Token_Factory::raw( "REPLACE(CHAR(32), ' ', " ),
$args[0],
- SQLiteTokenFactory::raw( ')' ),
+ WP_SQLite_Token_Factory::raw( ')' ),
)
);
case 'REPLACE':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( 'REPLACE(' ),
+ WP_SQLite_Token_Factory::raw( 'REPLACE(' ),
implode( ', ', $args ),
- SQLiteTokenFactory::raw( ')' ),
+ WP_SQLite_Token_Factory::raw( ')' ),
)
);
case 'RIGHT':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( 'SUBSTR(' ),
+ WP_SQLite_Token_Factory::raw( 'SUBSTR(' ),
$args[0],
- SQLiteTokenFactory::raw( ', -(' ),
+ WP_SQLite_Token_Factory::raw( ', -(' ),
$args[1],
- SQLiteTokenFactory::raw( '))' ),
+ WP_SQLite_Token_Factory::raw( '))' ),
)
);
case 'SPACE':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( "REPLACE(CHAR(32), ' ', '')" ),
+ WP_SQLite_Token_Factory::raw( "REPLACE(CHAR(32), ' ', '')" ),
)
);
case 'SUBSTRING':
case 'SUBSTR':
- return SQLiteTokenFactory::create_function( 'SUBSTR', $args );
+ return WP_SQLite_Token_Factory::create_function( 'SUBSTR', $args );
case 'UCASE':
case 'UPPER':
- return SQLiteTokenFactory::create_function( 'UPPER', $args );
+ return WP_SQLite_Token_Factory::create_function( 'UPPER', $args );
case 'DATE_FORMAT':
$mysql_date_format_to_sqlite_strftime = array(
@@ -540,113 +495,113 @@ private function translate_function_call( $function_call_tree ): SQLiteExpressio
$format = $args[1]->elements[0]->value;
$new_format = strtr( $format, $mysql_date_format_to_sqlite_strftime );
- return SQLiteTokenFactory::create_function(
+ return WP_SQLite_Token_Factory::create_function(
'STRFTIME',
array(
- new Expression( array( SQLiteTokenFactory::raw( $new_format ) ) ),
- new Expression( array( $args[0] ) ),
+ new WP_SQLite_Expression( array( WP_SQLite_Token_Factory::raw( $new_format ) ) ),
+ new WP_SQLite_Expression( array( $args[0] ) ),
)
);
case 'DATEDIFF':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::create_function( 'JULIANDAY', array( $args[0] ) ),
- SQLiteTokenFactory::raw( ' - ' ),
- SQLiteTokenFactory::create_function( 'JULIANDAY', array( $args[1] ) ),
+ WP_SQLite_Token_Factory::create_function( 'JULIANDAY', array( $args[0] ) ),
+ WP_SQLite_Token_Factory::raw( ' - ' ),
+ WP_SQLite_Token_Factory::create_function( 'JULIANDAY', array( $args[1] ) ),
)
);
case 'DAYNAME':
- return SQLiteTokenFactory::create_function(
+ return WP_SQLite_Token_Factory::create_function(
'STRFTIME',
- array( '%w', ...$args )
+ array_merge( array( '%w' ), $args )
);
case 'DAY':
case 'DAYOFMONTH':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( "CAST('" ),
- SQLiteTokenFactory::create_function( 'STRFTIME', array( '%d', ...$args ) ),
- SQLiteTokenFactory::raw( ") AS INTEGER'" ),
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%d' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
)
);
case 'DAYOFWEEK':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( "CAST('" ),
- SQLiteTokenFactory::create_function( 'STRFTIME', array( '%w', ...$args ) ),
- SQLiteTokenFactory::raw( ") + 1 AS INTEGER'" ),
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%w' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") + 1 AS INTEGER'" ),
)
);
case 'DAYOFYEAR':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( "CAST('" ),
- SQLiteTokenFactory::create_function( 'STRFTIME', array( '%j', ...$args ) ),
- SQLiteTokenFactory::raw( ") AS INTEGER'" ),
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%j' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
)
);
case 'HOUR':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( "CAST('" ),
- SQLiteTokenFactory::create_function( 'STRFTIME', array( '%H', ...$args ) ),
- SQLiteTokenFactory::raw( ") AS INTEGER'" ),
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%H' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
)
);
case 'MINUTE':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( "CAST('" ),
- SQLiteTokenFactory::create_function( 'STRFTIME', array( '%M', ...$args ) ),
- SQLiteTokenFactory::raw( ") AS INTEGER'" ),
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%M' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
)
);
case 'MONTH':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( "CAST('" ),
- SQLiteTokenFactory::create_function( 'STRFTIME', array( '%m', ...$args ) ),
- SQLiteTokenFactory::raw( ") AS INTEGER'" ),
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%m' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
)
);
case 'MONTHNAME':
- return SQLiteTokenFactory::create_function( 'STRFTIME', array( '%m', ...$args ) );
+ return WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%m' ), $args ) );
case 'NOW':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( 'CURRENT_TIMESTAMP()' ),
+ WP_SQLite_Token_Factory::raw( 'CURRENT_TIMESTAMP()' ),
)
);
case 'SECOND':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( "CAST('" ),
- SQLiteTokenFactory::create_function( 'STRFTIME', array( '%S', ...$args ) ),
- SQLiteTokenFactory::raw( ") AS INTEGER'" ),
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%S' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
)
);
case 'TIMESTAMP':
- return new Expression(
- array(
- SQLiteTokenFactory::raw( 'DATETIME(' ),
- ...$args,
- SQLiteTokenFactory::raw( ')' ),
+ return new WP_SQLite_Expression(
+ array_merge(
+ array( WP_SQLite_Token_Factory::raw( 'DATETIME(' ) ),
+ $args,
+ array( WP_SQLite_Token_Factory::raw( ')' ) )
)
);
case 'YEAR':
- return new Expression(
+ return new WP_SQLite_Expression(
array(
- SQLiteTokenFactory::raw( "CAST('" ),
- SQLiteTokenFactory::create_function( 'STRFTIME', array( '%Y', ...$args ) ),
- SQLiteTokenFactory::raw( ") AS INTEGER'" ),
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%Y' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
)
);
case 'FOUND_ROWS':
$this->has_found_rows_call = true;
- return new Expression(
+ return new WP_SQLite_Expression(
array(
// Post-processed in handleSqlCalcFoundRows()
- SQLiteTokenFactory::raw( 'FOUND_ROWS' ),
+ WP_SQLite_Token_Factory::raw( 'FOUND_ROWS' ),
)
);
default:
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-expression.php b/wp-includes/sqlite-ast/class-wp-sqlite-expression.php
new file mode 100644
index 0000000..99a947b
--- /dev/null
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-expression.php
@@ -0,0 +1,36 @@
+elements );
+ } else {
+ $new_elements[] = $element;
+ }
+ }
+ $this->elements = $new_elements;
+ }
+
+ public function get_tokens() {
+ return $this->elements;
+ }
+
+ public function add_token( WP_SQLite_Token $token ) {
+ $this->elements[] = $token;
+ }
+
+ public function add_tokens( array $tokens ) {
+ foreach ( $tokens as $token ) {
+ $this->add_token( $token );
+ }
+ }
+
+ public function add_expression( $expression ) {
+ $this->add_token( $expression );
+ }
+}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-query-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-query-builder.php
new file mode 100644
index 0000000..6c0906d
--- /dev/null
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-query-builder.php
@@ -0,0 +1,38 @@
+build_query();
+ }
+
+ public function __construct( WP_SQLite_Expression $expression ) {
+ $this->expression = $expression;
+ }
+
+ public function build_query(): string {
+ $query_parts = array();
+ foreach ( $this->expression->get_tokens() as $element ) {
+ if ( $element instanceof WP_SQLite_Token ) {
+ $query_parts[] = $this->process_token( $element );
+ } elseif ( $element instanceof WP_SQLite_Expression ) {
+ $query_parts[] = '(' . ( new self( $element ) )->build_query() . ')';
+ }
+ }
+ return implode( ' ', $query_parts );
+ }
+
+ private function process_token( WP_SQLite_Token $token ): string {
+ switch ( $token->type ) {
+ case WP_SQLite_Token::TYPE_OPERATOR:
+ case WP_SQLite_Token::TYPE_RAW:
+ case WP_SQLite_Token::TYPE_VALUE:
+ return $token->value;
+ case WP_SQLite_Token::TYPE_IDENTIFIER:
+ return '"' . str_replace( '"', '""', $token->value ) . '"';
+ default:
+ throw new InvalidArgumentException( 'Unknown token type: ' . $token->type );
+ }
+ }
+}
diff --git a/wip/SQLiteDriver.php b/wp-includes/sqlite-ast/class-wp-sqlite-token-factory.php
similarity index 64%
rename from wip/SQLiteDriver.php
rename to wp-includes/sqlite-ast/class-wp-sqlite-token-factory.php
index b5cad10..b803ae6 100644
--- a/wip/SQLiteDriver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-token-factory.php
@@ -1,11 +1,11 @@
-type = $type;
- $this->value = $value;
- }
-}
-
-class SQLiteQueryBuilder {
- private Expression $expression;
-
- public static function stringify( Expression $expression ) {
- return ( new SQLiteQueryBuilder( $expression ) )->build_query();
- }
-
- public function __construct( Expression $expression ) {
- $this->expression = $expression;
- }
-
- public function build_query(): string {
- $query_parts = array();
- foreach ( $this->expression->get_tokens() as $element ) {
- if ( $element instanceof SQLiteToken ) {
- $query_parts[] = $this->process_token( $element );
- } elseif ( $element instanceof Expression ) {
- $query_parts[] = '(' . ( new self( $element ) )->build_query() . ')';
- }
- }
- return implode( ' ', $query_parts );
- }
-
- private function process_token( SQLiteToken $token ): string {
- switch ( $token->type ) {
- case SQLiteToken::TYPE_RAW:
- case SQLiteToken::TYPE_OPERATOR:
- return $token->value;
- case SQLiteToken::TYPE_IDENTIFIER:
- return '"' . str_replace( '"', '""', $token->value ) . '"';
- case SQLiteToken::TYPE_VALUE:
- return $token->value;
- default:
- throw new InvalidArgumentException( 'Unknown token type: ' . $token->type );
- }
- }
-}
-
-class Expression {
-
- public $elements;
-
- public function __construct( array $elements = array() ) {
- $new_elements = array();
- $elements = array_filter( $elements, fn( $x ) => $x );
- foreach ( $elements as $element ) {
- if ( is_object( $element ) && $element instanceof Expression ) {
- $new_elements = array_merge( $new_elements, $element->elements );
- } else {
- $new_elements[] = $element;
- }
- }
- $this->elements = $new_elements;
- }
-
- public function get_tokens() {
- return $this->elements;
- }
-
- public function add_token( SQLiteToken $token ) {
- $this->elements[] = $token;
- }
-
- public function add_tokens( array $tokens ) {
- foreach ( $tokens as $token ) {
- $this->add_token( $token );
- }
- }
-
- public function add_expression( $expression ) {
- $this->add_token( $expression );
- }
-}
-
-class SQLiteExpression extends Expression {}
-
-class MySQLToSQLiteDriver {
-
- private $pdo;
-
- public function __construct( $dsn, $username = null, $password = null, $options = array() ) {
- /* phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO */
- $this->pdo = new PDO( $dsn, $username, $password, $options );
- }
-
- public function query( array $mysql_ast ) {
- $transformer = new SQLTransformer( $mysql_ast, 'sqlite' );
- $expression = $transformer->transform();
- if ( null !== $expression ) {
- $query_string = (string) $expression;
- return $this->pdo->query( $query_string );
- } else {
- throw new Exception( 'Failed to transform query.' );
- }
- }
-}
-
-// Example usage:
-
-// Sample parsed MySQL AST (Abstract Syntax Tree)
-// $ast = [
-// 'type' => 'select',
-// 'columns' => [
-// ['name' => '*', 'type' => 'ALL'],
-// ['name' => 'created_at', 'type' => 'DATETIME']
-// ],
-// 'from' => 'users',
-// 'keywords' => ['SELECT', 'FROM'],
-// 'options' => ['DISTINCT']
-// ];
-
-// try {
-// $driver = new MySQLToSQLiteDriver('sqlite::memory:');
-// $result = $driver->query($ast);
-// foreach ($result as $row) {
-// print_r($row);
-// }
-// } catch (Exception $e) {
-// echo $e->getMessage();
-// }
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-token.php b/wp-includes/sqlite-ast/class-wp-sqlite-token.php
new file mode 100644
index 0000000..8f5e3a0
--- /dev/null
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-token.php
@@ -0,0 +1,16 @@
+type = $type;
+ $this->value = $value;
+ }
+}
From 44641e14c1a79d5d18e3571bb1feee444c19192f Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 14 Nov 2024 14:54:28 +0100
Subject: [PATCH 002/124] Namespace-prefix the WIP SQLite driver for now to
avoid naming conflicts
---
tests/tools/dump-sqlite-query.php | 2 ++
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 11 +++++++++++
wp-includes/sqlite-ast/class-wp-sqlite-expression.php | 3 +++
.../sqlite-ast/class-wp-sqlite-query-builder.php | 5 +++++
.../sqlite-ast/class-wp-sqlite-token-factory.php | 5 +++++
wp-includes/sqlite-ast/class-wp-sqlite-token.php | 3 +++
6 files changed, 29 insertions(+)
diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php
index 4cf4fdc..a4594bd 100644
--- a/tests/tools/dump-sqlite-query.php
+++ b/tests/tools/dump-sqlite-query.php
@@ -12,6 +12,8 @@
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-token.php';
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-query-builder.php';
+use WIP\WP_SQLite_Driver;
+
$grammar_data = include __DIR__ . '/../../wp-includes/mysql/mysql-grammar.php';
$grammar = new WP_Parser_Grammar( $grammar_data );
$driver = new WP_SQLite_Driver( $grammar );
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 9f53f91..5ada962 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1,5 +1,16 @@
Date: Thu, 14 Nov 2024 15:37:45 +0100
Subject: [PATCH 003/124] Copy WP_SQLite_Translator_Tests and run them against
the SQLite driver
---
tests/WP_SQLite_Driver_Tests.php | 3374 +++++++++++++++++
tests/tools/dump-sqlite-query.php | 6 +-
.../sqlite-ast/class-wp-sqlite-driver.php | 52 +-
3 files changed, 3424 insertions(+), 8 deletions(-)
create mode 100644 tests/WP_SQLite_Driver_Tests.php
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
new file mode 100644
index 0000000..bee50ae
--- /dev/null
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -0,0 +1,3374 @@
+suppress_errors = false;
+ $GLOBALS['wpdb']->show_errors = true;
+ }
+ return;
+ }
+
+ // Before each test, we create a new database
+ public function setUp(): void {
+ $this->sqlite = new PDO( 'sqlite::memory:' );
+
+ $this->engine = new WP_SQLite_Driver( $this->sqlite );
+ $this->engine->query(
+ "CREATE TABLE _options (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name TEXT NOT NULL default '',
+ option_value TEXT NOT NULL default ''
+ );"
+ );
+ $this->engine->query(
+ "CREATE TABLE _dates (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name TEXT NOT NULL default '',
+ option_value DATE NOT NULL
+ );"
+ );
+ }
+
+ private function assertQuery( $sql, $error_substring = null ) {
+ $retval = $this->engine->query( $sql );
+ if ( null === $error_substring ) {
+ $this->assertEquals(
+ '',
+ $this->engine->get_error_message()
+ );
+ $this->assertNotFalse(
+ $retval
+ );
+ } else {
+ $this->assertStringContainsStringIgnoringCase( $error_substring, $this->engine->get_error_message() );
+ }
+
+ return $retval;
+ }
+
+ public function testRegexp() {
+ $this->assertQuery(
+ "INSERT INTO _options (option_name, option_value) VALUES ('rss_0123456789abcdef0123456789abcdef', '1');"
+ );
+ $this->assertQuery(
+ "INSERT INTO _options (option_name, option_value) VALUES ('transient', '1');"
+ );
+
+ $this->assertQuery( "DELETE FROM _options WHERE option_name REGEXP '^rss_.+$'" );
+ $this->assertQuery( 'SELECT * FROM _options' );
+ $this->assertCount( 1, $this->engine->get_query_results() );
+ }
+
+ /**
+ * @dataProvider regexpOperators
+ */
+ public function testRegexps( $operator, $regexp, $expected_result ) {
+ $this->assertQuery(
+ "INSERT INTO _options (option_name) VALUES ('rss_123'), ('RSS_123'), ('transient');"
+ );
+
+ $this->assertQuery( "SELECT ID, option_name FROM _options WHERE option_name $operator '$regexp' ORDER BY id LIMIT 1" );
+ $this->assertEquals(
+ array( $expected_result ),
+ $this->engine->get_query_results()
+ );
+ }
+
+ public static function regexpOperators() {
+ $lowercase_rss = (object) array(
+ 'ID' => '1',
+ 'option_name' => 'rss_123',
+ );
+ $uppercase_rss = (object) array(
+ 'ID' => '2',
+ 'option_name' => 'RSS_123',
+ );
+ $lowercase_transient = (object) array(
+ 'ID' => '3',
+ 'option_name' => 'transient',
+ );
+ return array(
+ array( 'REGEXP', '^RSS_.+$', $lowercase_rss ),
+ array( 'RLIKE', '^RSS_.+$', $lowercase_rss ),
+ array( 'REGEXP BINARY', '^RSS_.+$', $uppercase_rss ),
+ array( 'RLIKE BINARY', '^RSS_.+$', $uppercase_rss ),
+ array( 'NOT REGEXP', '^RSS_.+$', $lowercase_transient ),
+ array( 'NOT RLIKE', '^RSS_.+$', $lowercase_transient ),
+ array( 'NOT REGEXP BINARY', '^RSS_.+$', $lowercase_rss ),
+ array( 'NOT RLIKE BINARY', '^RSS_.+$', $lowercase_rss ),
+ );
+ }
+
+ public function testInsertDateNow() {
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('first', now());"
+ );
+
+ $this->assertQuery( 'SELECT YEAR(option_value) as y FROM _dates' );
+
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ $this->assertEquals( gmdate( 'Y' ), $results[0]->y );
+ }
+
+ public function testUpdateWithLimit() {
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 00:00:45');"
+ );
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-28 00:00:45');"
+ );
+
+ $this->assertQuery(
+ "UPDATE _dates SET option_value = '2001-05-27 10:08:48' WHERE option_name = 'first' ORDER BY option_name LIMIT 1;"
+ );
+
+ $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first';" );
+ $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second';" );
+
+ $this->assertEquals( '2001-05-27 10:08:48', $result1[0]->option_value );
+ $this->assertEquals( '2003-05-28 00:00:45', $result2[0]->option_value );
+
+ $this->assertQuery(
+ "UPDATE _dates SET option_value = '2001-05-27 10:08:49' WHERE option_name = 'first';"
+ );
+ $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first';" );
+ $this->assertEquals( '2001-05-27 10:08:49', $result1[0]->option_value );
+
+ $this->assertQuery(
+ "UPDATE _dates SET option_value = '2001-05-12 10:00:40' WHERE option_name in ( SELECT option_name from _dates );"
+ );
+ $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first';" );
+ $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second';" );
+ $this->assertEquals( '2001-05-12 10:00:40', $result1[0]->option_value );
+ $this->assertEquals( '2001-05-12 10:00:40', $result2[0]->option_value );
+ }
+
+ public function testUpdateWithLimitNoEndToken() {
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 00:00:45')"
+ );
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-28 00:00:45')"
+ );
+
+ $this->assertQuery(
+ "UPDATE _dates SET option_value = '2001-05-27 10:08:48' WHERE option_name = 'first' ORDER BY option_name LIMIT 1"
+ );
+ $results = $this->engine->get_query_results();
+
+ $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" );
+ $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" );
+
+ $this->assertEquals( '2001-05-27 10:08:48', $result1[0]->option_value );
+ $this->assertEquals( '2003-05-28 00:00:45', $result2[0]->option_value );
+
+ $this->assertQuery(
+ "UPDATE _dates SET option_value = '2001-05-27 10:08:49' WHERE option_name = 'first'"
+ );
+ $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" );
+ $this->assertEquals( '2001-05-27 10:08:49', $result1[0]->option_value );
+
+ $this->assertQuery(
+ "UPDATE _dates SET option_value = '2001-05-12 10:00:40' WHERE option_name in ( SELECT option_name from _dates )"
+ );
+ $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" );
+ $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" );
+ $this->assertEquals( '2001-05-12 10:00:40', $result1[0]->option_value );
+ $this->assertEquals( '2001-05-12 10:00:40', $result2[0]->option_value );
+ }
+
+ public function testUpdateWithoutWhereButWithSubSelect() {
+ $this->assertQuery(
+ "INSERT INTO _options (option_name, option_value) VALUES ('User 0000019', 'second');"
+ );
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');"
+ );
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-27 10:08:48');"
+ );
+ $return = $this->assertQuery(
+ "UPDATE _dates SET option_value = (SELECT option_value from _options WHERE option_name = 'User 0000019')"
+ );
+ $this->assertSame( 2, $return, 'UPDATE query did not return 2 when two row were changed' );
+
+ $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" );
+ $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" );
+ $this->assertEquals( 'second', $result1[0]->option_value );
+ $this->assertEquals( 'second', $result2[0]->option_value );
+ }
+
+ public function testUpdateWithoutWhereButWithLimit() {
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');"
+ );
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-27 10:08:48');"
+ );
+ $return = $this->assertQuery(
+ "UPDATE _dates SET option_value = 'second' LIMIT 1"
+ );
+ $this->assertSame( 1, $return, 'UPDATE query did not return 2 when two row were changed' );
+
+ $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" );
+ $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" );
+ $this->assertEquals( 'second', $result1[0]->option_value );
+ $this->assertEquals( '2003-05-27 10:08:48', $result2[0]->option_value );
+ }
+
+ public function testCastAsBinary() {
+ $this->assertQuery(
+ // Use a confusing alias to make sure it replaces only the correct token
+ "SELECT CAST('ABC' AS BINARY) as binary;"
+ );
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ $this->assertEquals( 'ABC', $results[0]->binary );
+ }
+
+ public function testSelectFromDual() {
+ $result = $this->assertQuery(
+ 'SELECT 1 as output FROM DUAL'
+ );
+ $this->assertEquals( 1, $result[0]->output );
+ }
+
+ public function testShowCreateTableNotFound() {
+ $this->assertQuery(
+ 'SHOW CREATE TABLE _no_such_table;'
+ );
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 0, $results );
+ }
+
+ public function testShowCreateTable1() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name VARCHAR(255) default '',
+ option_value TEXT NOT NULL,
+ UNIQUE KEY option_name (option_name),
+ KEY composite (option_name, option_value)
+ );"
+ );
+
+ $this->assertQuery(
+ 'SHOW CREATE TABLE _tmp_table;'
+ );
+ $results = $this->engine->get_query_results();
+ # TODO: Should we fix mismatch with original `option_value` text NOT NULL,` without default?
+ $this->assertEquals(
+ "CREATE TABLE `_tmp_table` (
+ `ID` bigint NOT NULL AUTO_INCREMENT,
+ `option_name` varchar(255) DEFAULT '',
+ `option_value` text NOT NULL DEFAULT '',
+ PRIMARY KEY (`ID`),
+ KEY `composite` (`option_name`, `option_value`),
+ UNIQUE KEY `option_name` (`option_name`)
+);",
+ $results[0]->{'Create Table'}
+ );
+ }
+
+ public function testShowCreateTableQuoted() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name VARCHAR(255) default '',
+ option_value TEXT NOT NULL,
+ UNIQUE KEY option_name (option_name),
+ KEY composite (option_name, option_value)
+ );"
+ );
+
+ $this->assertQuery(
+ 'SHOW CREATE TABLE `_tmp_table`;'
+ );
+ $results = $this->engine->get_query_results();
+ # TODO: Should we fix mismatch with original `option_value` text NOT NULL,` without default?
+ $this->assertEquals(
+ "CREATE TABLE `_tmp_table` (
+ `ID` bigint NOT NULL AUTO_INCREMENT,
+ `option_name` varchar(255) DEFAULT '',
+ `option_value` text NOT NULL DEFAULT '',
+ PRIMARY KEY (`ID`),
+ KEY `composite` (`option_name`, `option_value`),
+ UNIQUE KEY `option_name` (`option_name`)
+);",
+ $results[0]->{'Create Table'}
+ );
+ }
+
+ public function testShowCreateTableSimpleTable() {
+ $this->assertQuery(
+ 'CREATE TABLE _tmp_table (
+ ID BIGINT NOT NULL
+ );'
+ );
+
+ $this->assertQuery(
+ 'SHOW CREATE TABLE _tmp_table;'
+ );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ 'CREATE TABLE `_tmp_table` (
+ `ID` bigint NOT NULL DEFAULT 0
+);',
+ $results[0]->{'Create Table'}
+ );
+ }
+
+ public function testShowCreateTableWithAlterAndCreateIndex() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name VARCHAR(255) default '',
+ option_value TEXT NOT NULL
+ );"
+ );
+
+ $this->assertQuery(
+ 'ALTER TABLE _tmp_table CHANGE COLUMN option_name option_name SMALLINT NOT NULL default 14'
+ );
+
+ $this->assertQuery(
+ 'ALTER TABLE _tmp_table ADD INDEX option_name (option_name);'
+ );
+
+ $this->assertQuery(
+ 'SHOW CREATE TABLE _tmp_table;'
+ );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ 'CREATE TABLE `_tmp_table` (
+ `ID` bigint NOT NULL AUTO_INCREMENT,
+ `option_name` smallint NOT NULL DEFAULT 14,
+ `option_value` text NOT NULL DEFAULT \'\',
+ PRIMARY KEY (`ID`),
+ KEY `option_name` (`option_name`)
+);',
+ $results[0]->{'Create Table'}
+ );
+ }
+
+ public function testCreateTablseWithIdenticalIndexNames() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table_a (
+ ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name VARCHAR(255) default '',
+ option_value TEXT NOT NULL,
+ KEY `option_name` (`option_name`),
+ KEY `double__underscores` (`option_name`, `ID`)
+ );"
+ );
+
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table_b (
+ ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name VARCHAR(255) default '',
+ option_value TEXT NOT NULL,
+ KEY `option_name` (`option_name`),
+ KEY `double__underscores` (`option_name`, `ID`)
+ );"
+ );
+ }
+
+ public function testShowCreateTablePreservesDoubleUnderscoreKeyNames() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp__table (
+ ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name VARCHAR(255) default '',
+ option_value TEXT NOT NULL,
+ KEY `option_name` (`option_name`),
+ KEY `double__underscores` (`option_name`, `ID`)
+ );"
+ );
+
+ $this->assertQuery(
+ 'SHOW CREATE TABLE _tmp__table;'
+ );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ 'CREATE TABLE `_tmp__table` (
+ `ID` bigint NOT NULL AUTO_INCREMENT,
+ `option_name` varchar(255) DEFAULT \'\',
+ `option_value` text NOT NULL DEFAULT \'\',
+ PRIMARY KEY (`ID`),
+ KEY `double__underscores` (`option_name`, `ID`),
+ KEY `option_name` (`option_name`)
+);',
+ $results[0]->{'Create Table'}
+ );
+ }
+
+ public function testShowCreateTableWithPrimaryKeyColumnsReverseOrdered() {
+ $this->assertQuery(
+ 'CREATE TABLE `_tmp_table` (
+ `ID_A` BIGINT NOT NULL,
+ `ID_B` BIGINT NOT NULL,
+ `ID_C` BIGINT NOT NULL,
+ PRIMARY KEY (`ID_B`, `ID_A`, `ID_C`)
+ );'
+ );
+
+ $this->assertQuery(
+ 'SHOW CREATE TABLE _tmp_table;'
+ );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ 'CREATE TABLE `_tmp_table` (
+ `ID_A` bigint NOT NULL DEFAULT 0,
+ `ID_B` bigint NOT NULL DEFAULT 0,
+ `ID_C` bigint NOT NULL DEFAULT 0,
+ PRIMARY KEY (`ID_B`, `ID_A`, `ID_C`)
+);',
+ $results[0]->{'Create Table'}
+ );
+ }
+
+ public function testShowCreateTableWithColumnKeys() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ `ID` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ `option_name` varchar(255) DEFAULT '',
+ `option_value` text NOT NULL DEFAULT '',
+ KEY _tmp_table__composite (option_name, option_value),
+ UNIQUE KEY _tmp_table__option_name (option_name) );"
+ );
+ }
+
+ public function testShowCreateTableWithCorrectDefaultValues() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp__table (
+ ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ default_empty_string VARCHAR(255) default '',
+ null_no_default VARCHAR(255),
+ );"
+ );
+
+ $this->assertQuery(
+ 'SHOW CREATE TABLE _tmp__table;'
+ );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ 'CREATE TABLE `_tmp__table` (
+ `ID` bigint NOT NULL AUTO_INCREMENT,
+ `default_empty_string` varchar(255) DEFAULT \'\',
+ `null_no_default` varchar(255),
+ PRIMARY KEY (`ID`)
+);',
+ $results[0]->{'Create Table'}
+ );
+ }
+
+ public function testSelectIndexHintForce() {
+ $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" );
+ $result = $this->assertQuery(
+ 'SELECT 1 as output FROM _options FORCE INDEX (PRIMARY, post_parent) WHERE 1=1'
+ );
+ $this->assertEquals( 1, $result[0]->output );
+ }
+
+ public function testSelectIndexHintUseGroup() {
+ $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" );
+ $result = $this->assertQuery(
+ 'SELECT 1 as output FROM _options USE KEY FOR GROUP BY (PRIMARY, post_parent) WHERE 1=1'
+ );
+ $this->assertEquals( 1, $result[0]->output );
+ }
+
+ public function testLeftFunction1Char() {
+ $result = $this->assertQuery(
+ 'SELECT LEFT("abc", 1) as output'
+ );
+ $this->assertEquals( 'a', $result[0]->output );
+ }
+
+ public function testLeftFunction5Chars() {
+ $result = $this->assertQuery(
+ 'SELECT LEFT("Lorem ipsum", 5) as output'
+ );
+ $this->assertEquals( 'Lorem', $result[0]->output );
+ }
+
+ public function testLeftFunctionNullString() {
+ $result = $this->assertQuery(
+ 'SELECT LEFT(NULL, 5) as output'
+ );
+ $this->assertEquals( null, $result[0]->output );
+ }
+
+ public function testLeftFunctionNullLength() {
+ $result = $this->assertQuery(
+ 'SELECT LEFT("Test", NULL) as output'
+ );
+ $this->assertEquals( null, $result[0]->output );
+ }
+
+ public function testInsertSelectFromDual() {
+ $result = $this->assertQuery(
+ 'INSERT INTO _options (option_name, option_value) SELECT "A", "b" FROM DUAL WHERE ( SELECT NULL FROM DUAL ) IS NULL'
+ );
+ $this->assertEquals( 1, $result );
+ }
+
+ public function testCreateTemporaryTable() {
+ $this->assertQuery(
+ "CREATE TEMPORARY TABLE _tmp_table (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name TEXT NOT NULL default '',
+ option_value TEXT NOT NULL default ''
+ );"
+ );
+ $this->assertQuery(
+ 'DROP TEMPORARY TABLE _tmp_table;'
+ );
+ }
+
+ public function testShowTablesLike() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name TEXT NOT NULL default '',
+ option_value TEXT NOT NULL default ''
+ );"
+ );
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table_2 (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name TEXT NOT NULL default '',
+ option_value TEXT NOT NULL default ''
+ );"
+ );
+
+ $this->assertQuery(
+ "SHOW TABLES LIKE '_tmp_table';"
+ );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Tables_in_db' => '_tmp_table',
+ ),
+ ),
+ $this->engine->get_query_results()
+ );
+ }
+
+ public function testShowTableStatusFrom() {
+ // Created in setUp() function
+ $this->assertQuery( 'DROP TABLE _options' );
+ $this->assertQuery( 'DROP TABLE _dates' );
+
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name TEXT NOT NULL default '',
+ option_value TEXT NOT NULL default ''
+ );"
+ );
+
+ $this->assertQuery(
+ "SHOW TABLE STATUS FROM 'mydb';"
+ );
+
+ $this->assertCount(
+ 1,
+ $this->engine->get_query_results()
+ );
+ }
+
+ public function testShowTableStatusIn() {
+ // Created in setUp() function
+ $this->assertQuery( 'DROP TABLE _options' );
+ $this->assertQuery( 'DROP TABLE _dates' );
+
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name TEXT NOT NULL default '',
+ option_value TEXT NOT NULL default ''
+ );"
+ );
+
+ $this->assertQuery(
+ "SHOW TABLE STATUS IN 'mydb';"
+ );
+
+ $this->assertCount(
+ 1,
+ $this->engine->get_query_results()
+ );
+ }
+
+ public function testShowTableStatusInTwoTables() {
+ // Created in setUp() function
+ $this->assertQuery( 'DROP TABLE _options' );
+ $this->assertQuery( 'DROP TABLE _dates' );
+
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name TEXT NOT NULL default '',
+ option_value TEXT NOT NULL default ''
+ );"
+ );
+
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table2 (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name TEXT NOT NULL default '',
+ option_value TEXT NOT NULL default ''
+ );"
+ );
+ $this->assertQuery(
+ "SHOW TABLE STATUS IN 'mydb';"
+ );
+
+ $this->assertCount(
+ 2,
+ $this->engine->get_query_results()
+ );
+ }
+
+ public function testShowTableStatusLike() {
+ // Created in setUp() function
+ $this->assertQuery( 'DROP TABLE _options' );
+ $this->assertQuery( 'DROP TABLE _dates' );
+
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table1 (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name TEXT NOT NULL default '',
+ option_value TEXT NOT NULL default ''
+ );"
+ );
+
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table2 (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name TEXT NOT NULL default '',
+ option_value TEXT NOT NULL default ''
+ );"
+ );
+
+ $this->assertQuery(
+ "SHOW TABLE STATUS LIKE '_tmp_table%';"
+ );
+ $this->assertCount(
+ 2,
+ $this->engine->get_query_results()
+ );
+ $this->assertEquals(
+ '_tmp_table1',
+ $this->engine->get_query_results()[0]->Name
+ );
+ }
+
+ public function testCreateTable() {
+ $result = $this->assertQuery(
+ "CREATE TABLE wptests_users (
+ ID bigint(20) unsigned NOT NULL auto_increment,
+ user_login varchar(60) NOT NULL default '',
+ user_pass varchar(255) NOT NULL default '',
+ user_nicename varchar(50) NOT NULL default '',
+ user_email varchar(100) NOT NULL default '',
+ user_url varchar(100) NOT NULL default '',
+ user_registered datetime NOT NULL default '0000-00-00 00:00:00',
+ user_activation_key varchar(255) NOT NULL default '',
+ user_status int(11) NOT NULL default '0',
+ display_name varchar(250) NOT NULL default '',
+ PRIMARY KEY (ID),
+ KEY user_login_key (user_login),
+ KEY user_nicename (user_nicename),
+ KEY user_email (user_email)
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci"
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $this->assertQuery( 'DESCRIBE wptests_users;' );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'ID',
+ 'Type' => 'bigint(20) unsigned',
+ 'Null' => 'NO',
+ 'Key' => 'PRI',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'user_login',
+ 'Type' => 'varchar(60)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'user_pass',
+ 'Type' => 'varchar(255)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'user_nicename',
+ 'Type' => 'varchar(50)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'user_email',
+ 'Type' => 'varchar(100)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'user_url',
+ 'Type' => 'varchar(100)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'user_registered',
+ 'Type' => 'datetime',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0000-00-00 00:00:00',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'user_activation_key',
+ 'Type' => 'varchar(255)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'user_status',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'display_name',
+ 'Type' => 'varchar(250)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+ }
+
+ public function testCreateTableWithTrailingComma() {
+ $result = $this->assertQuery(
+ 'CREATE TABLE wptests_users (
+ ID bigint(20) unsigned NOT NULL auto_increment,
+ PRIMARY KEY (ID),
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci'
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+ }
+
+ public function testCreateTableSpatialIndex() {
+ $result = $this->assertQuery(
+ 'CREATE TABLE wptests_users (
+ ID bigint(20) unsigned NOT NULL auto_increment,
+ UNIQUE KEY (ID),
+ )'
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+ }
+
+ public function testCreateTableWithMultiValueColumnTypeModifiers() {
+ $result = $this->assertQuery(
+ "CREATE TABLE wptests_users (
+ ID bigint(20) unsigned NOT NULL auto_increment,
+ decimal_column DECIMAL(10,2) NOT NULL DEFAULT 0,
+ float_column FLOAT(10,2) NOT NULL DEFAULT 0,
+ enum_column ENUM('a', 'b', 'c') NOT NULL DEFAULT 'a',
+ PRIMARY KEY (ID),
+ )"
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $this->assertQuery( 'DESCRIBE wptests_users;' );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'ID',
+ 'Type' => 'bigint(20) unsigned',
+ 'Null' => 'NO',
+ 'Key' => 'PRI',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'decimal_column',
+ 'Type' => 'decimal(10,2)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => 0,
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'float_column',
+ 'Type' => 'float(10,2)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => 0,
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'enum_column',
+ 'Type' => "enum('a','b','c')",
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => 'a',
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+ }
+
+ public function testAlterTableAddAndDropColumn() {
+ $result = $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ name varchar(20) NOT NULL default ''
+ );"
+ );
+
+ $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD COLUMN `column` int;' );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'name',
+ 'Type' => 'varchar(20)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'column',
+ 'Type' => 'int',
+ 'Null' => 'YES',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+
+ $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD `column2` int;' );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'name',
+ 'Type' => 'varchar(20)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'column',
+ 'Type' => 'int',
+ 'Null' => 'YES',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'column2',
+ 'Type' => 'int',
+ 'Null' => 'YES',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+
+ $result = $this->assertQuery( 'ALTER TABLE _tmp_table DROP COLUMN `column`;' );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'name',
+ 'Type' => 'varchar(20)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'column2',
+ 'Type' => 'int',
+ 'Null' => 'YES',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+
+ $result = $this->assertQuery( 'ALTER TABLE _tmp_table DROP `column2`;' );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'name',
+ 'Type' => 'varchar(20)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+ }
+
+ public function testAlterTableAddNotNullVarcharColumn() {
+ $result = $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ name varchar(20) NOT NULL default ''
+ );"
+ );
+
+ $result = $this->assertQuery( "ALTER TABLE _tmp_table ADD COLUMN `column` VARCHAR(20) NOT NULL DEFAULT 'foo';" );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'name',
+ 'Type' => 'varchar(20)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'column',
+ 'Type' => 'varchar(20)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => 'foo',
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+ }
+
+ public function testColumnWithOnUpdate() {
+ // CREATE TABLE with ON UPDATE
+ $this->assertQuery(
+ 'CREATE TABLE _tmp_table (
+ id int(11) NOT NULL,
+ created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP
+ );'
+ );
+ $results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'id',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'created_at',
+ 'Type' => 'timestamp',
+ 'Null' => 'YES',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+
+ // ADD COLUMN with ON UPDATE
+ $this->assertQuery(
+ 'ALTER TABLE _tmp_table ADD COLUMN updated_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP'
+ );
+ $results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'id',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'created_at',
+ 'Type' => 'timestamp',
+ 'Null' => 'YES',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'updated_at',
+ 'Type' => 'timestamp',
+ 'Null' => 'YES',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+
+ // assert ON UPDATE triggers
+ $results = $this->assertQuery( "SELECT * FROM sqlite_master WHERE type = 'trigger'" );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'type' => 'trigger',
+ 'name' => '___tmp_table_created_at_on_update__',
+ 'tbl_name' => '_tmp_table',
+ 'rootpage' => '0',
+ 'sql' => "CREATE TRIGGER \"___tmp_table_created_at_on_update__\"\n\t\t\tAFTER UPDATE ON \"_tmp_table\"\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE \"_tmp_table\" SET \"created_at\" = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;\n\t\t\tEND",
+ ),
+ (object) array(
+ 'type' => 'trigger',
+ 'name' => '___tmp_table_updated_at_on_update__',
+ 'tbl_name' => '_tmp_table',
+ 'rootpage' => '0',
+ 'sql' => "CREATE TRIGGER \"___tmp_table_updated_at_on_update__\"\n\t\t\tAFTER UPDATE ON \"_tmp_table\"\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE \"_tmp_table\" SET \"updated_at\" = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;\n\t\t\tEND",
+ ),
+ ),
+ $results
+ );
+
+ // on INSERT, no timestamps are expected
+ $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (1)' );
+ $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 1' );
+ $this->assertNull( $result[0]->created_at );
+ $this->assertNull( $result[0]->updated_at );
+
+ // on UPDATE, we expect timestamps in form YYYY-MM-DD HH:MM:SS
+ $this->assertQuery( 'UPDATE _tmp_table SET id = 2 WHERE id = 1' );
+ $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 2' );
+ $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->created_at );
+ $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->updated_at );
+
+ // drop ON UPDATE
+ $this->assertQuery(
+ 'ALTER TABLE _tmp_table
+ CHANGE created_at created_at timestamp NULL,
+ CHANGE COLUMN updated_at updated_at timestamp NULL'
+ );
+ $results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'id',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'created_at',
+ 'Type' => 'timestamp',
+ 'Null' => 'YES',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'updated_at',
+ 'Type' => 'timestamp',
+ 'Null' => 'YES',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+
+ // assert ON UPDATE triggers are removed
+ $results = $this->assertQuery( "SELECT * FROM sqlite_master WHERE type = 'trigger'" );
+ $this->assertEquals( array(), $results );
+
+ // now, no timestamps are expected
+ $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (10)' );
+ $this->assertQuery( 'UPDATE _tmp_table SET id = 11 WHERE id = 10' );
+ $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 11' );
+ $this->assertNull( $result[0]->created_at );
+ $this->assertNull( $result[0]->updated_at );
+ }
+
+ public function testColumnWithOnUpdateAndNoIdField() {
+ // CREATE TABLE with ON UPDATE
+ $this->assertQuery(
+ 'CREATE TABLE _tmp_table (
+ name varchar(20) NOT NULL,
+ created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP
+ );'
+ );
+
+ // on INSERT, no timestamps are expected
+ $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('aaa')" );
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name = 'aaa'" );
+ $this->assertNull( $result[0]->created_at );
+
+ // on UPDATE, we expect timestamps in form YYYY-MM-DD HH:MM:SS
+ $this->assertQuery( "UPDATE _tmp_table SET name = 'bbb' WHERE name = 'aaa'" );
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name = 'bbb'" );
+ $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->created_at );
+ }
+
+ public function testChangeColumnWithOnUpdate() {
+ // CREATE TABLE with ON UPDATE
+ $this->assertQuery(
+ 'CREATE TABLE _tmp_table (
+ id int(11) NOT NULL,
+ created_at timestamp NULL
+ );'
+ );
+ $results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'id',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'created_at',
+ 'Type' => 'timestamp',
+ 'Null' => 'YES',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+
+ // no ON UPDATE is set
+ $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (1)' );
+ $this->assertQuery( 'UPDATE _tmp_table SET id = 1 WHERE id = 1' );
+ $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 1' );
+ $this->assertNull( $result[0]->created_at );
+
+ // CHANGE COLUMN to add ON UPDATE
+ $this->assertQuery(
+ 'ALTER TABLE _tmp_table CHANGE COLUMN created_at created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP'
+ );
+ $results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'id',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'created_at',
+ 'Type' => 'timestamp',
+ 'Null' => 'YES',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+
+ // now, ON UPDATE SHOULD BE SET
+ $this->assertQuery( 'UPDATE _tmp_table SET id = 1 WHERE id = 1' );
+ $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 1' );
+ $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->created_at );
+
+ // change column to remove ON UPDATE
+ $this->assertQuery(
+ 'ALTER TABLE _tmp_table CHANGE COLUMN created_at created_at timestamp NULL'
+ );
+ $results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'id',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'created_at',
+ 'Type' => 'timestamp',
+ 'Null' => 'YES',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+
+ // now, no timestamp is expected
+ $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (2)' );
+ $this->assertQuery( 'UPDATE _tmp_table SET id = 2 WHERE id = 2' );
+ $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 2' );
+ $this->assertNull( $result[0]->created_at );
+ }
+
+ public function testAlterTableWithColumnFirstAndAfter() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ id int(11) NOT NULL,
+ name varchar(20) NOT NULL default ''
+ );"
+ );
+
+ // ADD COLUMN with FIRST
+ $this->assertQuery(
+ "ALTER TABLE _tmp_table ADD COLUMN new_first_column VARCHAR(255) NOT NULL DEFAULT '' FIRST"
+ );
+ $results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'id',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'name',
+ 'Type' => 'varchar(20)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'new_first_column',
+ 'Type' => 'varchar(255)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+
+ // ADD COLUMN with AFTER
+ $this->assertQuery(
+ "ALTER TABLE _tmp_table ADD COLUMN new_column VARCHAR(255) NOT NULL DEFAULT '' AFTER id"
+ );
+ $results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'id',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'name',
+ 'Type' => 'varchar(20)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'new_first_column',
+ 'Type' => 'varchar(255)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'new_column',
+ 'Type' => 'varchar(255)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+
+ // CHANGE with FIRST
+ $this->assertQuery(
+ "ALTER TABLE _tmp_table CHANGE id id int(11) NOT NULL DEFAULT '0' FIRST"
+ );
+ $results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'id',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'name',
+ 'Type' => 'varchar(20)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'new_first_column',
+ 'Type' => 'varchar(255)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'new_column',
+ 'Type' => 'varchar(255)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+
+ // CHANGE with AFTER
+ $this->assertQuery(
+ "ALTER TABLE _tmp_table CHANGE id id int(11) NOT NULL DEFAULT '0' AFTER name"
+ );
+ $results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'id',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'name',
+ 'Type' => 'varchar(20)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'new_first_column',
+ 'Type' => 'varchar(255)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'new_column',
+ 'Type' => 'varchar(255)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+ }
+
+ public function testAlterTableWithMultiColumnFirstAndAfter() {
+ $this->assertQuery(
+ 'CREATE TABLE _tmp_table (
+ id int(11) NOT NULL
+ );'
+ );
+
+ // ADD COLUMN
+ $this->assertQuery(
+ 'ALTER TABLE _tmp_table
+ ADD COLUMN new1 varchar(255) NOT NULL,
+ ADD COLUMN new2 varchar(255) NOT NULL FIRST,
+ ADD COLUMN new3 varchar(255) NOT NULL AFTER new1'
+ );
+ $results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'id',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'new1',
+ 'Type' => 'varchar(255)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'new2',
+ 'Type' => 'varchar(255)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'new3',
+ 'Type' => 'varchar(255)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+
+ // CHANGE
+ $this->assertQuery(
+ 'ALTER TABLE _tmp_table
+ CHANGE new1 new1 int(11) NOT NULL FIRST,
+ CHANGE new2 new2 int(11) NOT NULL,
+ CHANGE new3 new3 int(11) NOT NULL AFTER new2'
+ );
+ $results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'id',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'new1',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'new2',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'new3',
+ 'Type' => 'int(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ ),
+ $results
+ );
+ }
+
+ public function testAlterTableAddIndex() {
+ $result = $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ name varchar(20) NOT NULL default ''
+ );"
+ );
+
+ $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD INDEX name (name);' );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $this->assertQuery( 'SHOW INDEX FROM _tmp_table;' );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Table' => '_tmp_table',
+ 'Non_unique' => '1',
+ 'Key_name' => 'name',
+ 'Seq_in_index' => '0',
+ 'Column_name' => 'name',
+ 'Collation' => 'A',
+ 'Cardinality' => '0',
+ 'Sub_part' => null,
+ 'Packed' => null,
+ 'Null' => '',
+ 'Index_type' => 'BTREE',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ ),
+ $results
+ );
+ }
+
+ public function testAlterTableAddUniqueIndex() {
+ $result = $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ name varchar(20) NOT NULL default ''
+ );"
+ );
+
+ $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD UNIQUE INDEX name (name(20));' );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $this->assertQuery( 'SHOW INDEX FROM _tmp_table;' );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Table' => '_tmp_table',
+ 'Non_unique' => '0',
+ 'Key_name' => 'name',
+ 'Seq_in_index' => '0',
+ 'Column_name' => 'name',
+ 'Collation' => 'A',
+ 'Cardinality' => '0',
+ 'Sub_part' => null,
+ 'Packed' => null,
+ 'Null' => '',
+ 'Index_type' => 'BTREE',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ ),
+ $results
+ );
+ }
+
+ public function testAlterTableAddFulltextIndex() {
+ $result = $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ name varchar(20) NOT NULL default ''
+ );"
+ );
+
+ $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD FULLTEXT INDEX name (name);' );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $this->assertQuery( 'SHOW INDEX FROM _tmp_table;' );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Table' => '_tmp_table',
+ 'Non_unique' => '1',
+ 'Key_name' => 'name',
+ 'Seq_in_index' => '0',
+ 'Column_name' => 'name',
+ 'Collation' => 'A',
+ 'Cardinality' => '0',
+ 'Sub_part' => null,
+ 'Packed' => null,
+ 'Null' => '',
+ 'Index_type' => 'FULLTEXT',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ ),
+ $results
+ );
+ }
+
+ public function testAlterTableModifyColumn() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ name varchar(20) NOT NULL default '',
+ lastname varchar(20) NOT NULL default '',
+ KEY composite (name, lastname),
+ UNIQUE KEY name (name)
+ );"
+ );
+ // Insert a record
+ $result = $this->assertQuery( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Johnny', 'Appleseed');" );
+ $this->assertEquals( 1, $result );
+
+ // Primary key violation:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Mike', 'Pearseed');" );
+ $this->assertEquals( false, $result );
+
+ // Unique constraint violation:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (2, 'Johnny', 'Appleseed');" );
+ $this->assertEquals( false, $result );
+
+ // Rename the "name" field to "firstname":
+ $result = $this->engine->query( "ALTER TABLE _tmp_table CHANGE column name firstname varchar(50) NOT NULL default 'mark';" );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ // Confirm the original data is still there:
+ $result = $this->engine->query( 'SELECT * FROM _tmp_table;' );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 1, $result[0]->ID );
+ $this->assertEquals( 'Johnny', $result[0]->firstname );
+ $this->assertEquals( 'Appleseed', $result[0]->lastname );
+
+ // Confirm the primary key is intact:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (1, 'Mike', 'Pearseed');" );
+ $this->assertEquals( false, $result );
+
+ // Confirm the unique key is intact:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (2, 'Johnny', 'Appleseed');" );
+ $this->assertEquals( false, $result );
+
+ // Confirm the autoincrement still works:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (firstname, lastname) VALUES ('John', 'Doe');" );
+ $this->assertEquals( true, $result );
+ $result = $this->engine->query( "SELECT * FROM _tmp_table WHERE firstname='John';" );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 2, $result[0]->ID );
+ }
+
+
+ public function testAlterTableModifyColumnWithSkippedColumnKeyword() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ name varchar(20) NOT NULL default '',
+ lastname varchar(20) NOT NULL default '',
+ KEY composite (name, lastname),
+ UNIQUE KEY name (name)
+ );"
+ );
+ // Insert a record
+ $result = $this->assertQuery( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Johnny', 'Appleseed');" );
+ $this->assertEquals( 1, $result );
+
+ // Primary key violation:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Mike', 'Pearseed');" );
+ $this->assertEquals( false, $result );
+
+ // Unique constraint violation:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (2, 'Johnny', 'Appleseed');" );
+ $this->assertEquals( false, $result );
+
+ // Rename the "name" field to "firstname":
+ $result = $this->engine->query( "ALTER TABLE _tmp_table CHANGE name firstname varchar(50) NOT NULL default 'mark';" );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ // Confirm the original data is still there:
+ $result = $this->engine->query( 'SELECT * FROM _tmp_table;' );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 1, $result[0]->ID );
+ $this->assertEquals( 'Johnny', $result[0]->firstname );
+ $this->assertEquals( 'Appleseed', $result[0]->lastname );
+
+ // Confirm the primary key is intact:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (1, 'Mike', 'Pearseed');" );
+ $this->assertEquals( false, $result );
+
+ // Confirm the unique key is intact:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (2, 'Johnny', 'Appleseed');" );
+ $this->assertEquals( false, $result );
+
+ // Confirm the autoincrement still works:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (firstname, lastname) VALUES ('John', 'Doe');" );
+ $this->assertEquals( true, $result );
+ $result = $this->engine->query( "SELECT * FROM _tmp_table WHERE firstname='John';" );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 2, $result[0]->ID );
+ }
+
+ public function testAlterTableModifyColumnWithHyphens() {
+ $result = $this->assertQuery(
+ 'CREATE TABLE wptests_dbdelta_test2 (
+ `foo-bar` varchar(255) DEFAULT NULL
+ )'
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $result = $this->assertQuery(
+ 'ALTER TABLE wptests_dbdelta_test2 CHANGE COLUMN `foo-bar` `foo-bar` text DEFAULT NULL'
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $result = $this->assertQuery( 'DESCRIBE wptests_dbdelta_test2;' );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertNotFalse( $result );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'foo-bar',
+ 'Type' => 'text',
+ 'Null' => 'YES',
+ 'Key' => '',
+ 'Default' => 'NULL',
+ 'Extra' => '',
+ ),
+ ),
+ $result
+ );
+ }
+
+ public function testAlterTableModifyColumnComplexChange() {
+ $result = $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID INTEGER NOT NULL,
+ name varchar(20) NOT NULL default '',
+ lastname varchar(20) default '',
+ date_as_string varchar(20) default '',
+ PRIMARY KEY (ID, name)
+ );"
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ // Add a unique index
+ $result = $this->assertQuery(
+ 'ALTER TABLE _tmp_table ADD UNIQUE INDEX "test_unique_composite" (name, lastname);'
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ // Add a regular index
+ $result = $this->assertQuery(
+ 'ALTER TABLE _tmp_table ADD INDEX "test_regular" (lastname);'
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ // Confirm the table is well-behaved so far:
+
+ // Insert a few records
+ $result = $this->assertQuery(
+ "
+ INSERT INTO _tmp_table (ID, name, lastname, date_as_string)
+ VALUES
+ (1, 'Johnny', 'Appleseed', '2002-01-01 12:53:13'),
+ (2, 'Mike', 'Foo', '2003-01-01 12:53:13'),
+ (3, 'Kate', 'Bar', '2004-01-01 12:53:13'),
+ (4, 'Anna', 'Pear', '2005-01-01 12:53:13')
+ ;"
+ );
+ $this->assertEquals( 4, $result );
+
+ // Primary key violation:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name) VALUES (1, 'Johnny');" );
+ $this->assertEquals( false, $result );
+
+ // Unique constraint violation:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (5, 'Kate', 'Bar');" );
+ $this->assertEquals( false, $result );
+
+ // No constraint violation:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (5, 'Joanna', 'Bar');" );
+ $this->assertEquals( 1, $result );
+
+ // Now – let's change a few columns:
+ $result = $this->engine->query( 'ALTER TABLE _tmp_table CHANGE COLUMN name firstname varchar(20)' );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $result = $this->engine->query( 'ALTER TABLE _tmp_table CHANGE COLUMN date_as_string datetime datetime NOT NULL' );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ // Finally, let's confirm our data is intact and the table is still well-behaved:
+ $result = $this->engine->query( 'SELECT * FROM _tmp_table ORDER BY ID;' );
+ $this->assertCount( 5, $result );
+ $this->assertEquals( 1, $result[0]->ID );
+ $this->assertEquals( 'Johnny', $result[0]->firstname );
+ $this->assertEquals( 'Appleseed', $result[0]->lastname );
+ $this->assertEquals( '2002-01-01 12:53:13', $result[0]->datetime );
+
+ // Primary key violation:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, datetime) VALUES (1, 'Johnny', '2010-01-01 12:53:13');" );
+ $this->assertEquals( false, $result );
+
+ // Unique constraint violation:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname, datetime) VALUES (6, 'Kate', 'Bar', '2010-01-01 12:53:13');" );
+ $this->assertEquals( false, $result );
+
+ // No constraint violation:
+ $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname, datetime) VALUES (6, 'Sophie', 'Bar', '2010-01-01 12:53:13');" );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+ }
+
+ public function testCaseInsensitiveUniqueIndex() {
+ $result = $this->engine->query(
+ "CREATE TABLE _tmp_table (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ name varchar(20) NOT NULL default '',
+ lastname varchar(20) NOT NULL default '',
+ KEY name (name),
+ UNIQUE KEY uname (name),
+ UNIQUE KEY last (lastname)
+ );"
+ );
+ $this->assertEquals( 1, $result );
+
+ $result1 = $this->engine->query( "INSERT INTO _tmp_table (name, lastname) VALUES ('first', 'last');" );
+ $this->assertEquals( 1, $result1 );
+
+ $result1 = $this->engine->query( 'SELECT COUNT(*) num FROM _tmp_table;' );
+ $this->assertEquals( 1, $result1[0]->num );
+
+ // Unique keys should be case-insensitive:
+ $result2 = $this->assertQuery(
+ "INSERT INTO _tmp_table (name, lastname) VALUES ('FIRST', 'LAST' );",
+ 'UNIQUE constraint failed'
+ );
+
+ $this->assertEquals( false, $result2 );
+
+ $result1 = $this->engine->query( 'SELECT COUNT(*) num FROM _tmp_table;' );
+ $this->assertEquals( 1, $result1[0]->num );
+
+ // Unique keys should be case-insensitive:
+ $result1 = $this->assertQuery(
+ "INSERT IGNORE INTO _tmp_table (name) VALUES ('FIRST');"
+ );
+
+ self::assertEquals( 0, $result1 );
+
+ $result2 = $this->engine->get_query_results();
+ $this->assertEquals( 0, $result2 );
+
+ $result1 = $this->engine->query( 'SELECT COUNT(*)num FROM _tmp_table;' );
+ $this->assertEquals( 1, $result1[0]->num );
+
+ // Unique keys should be case-insensitive:
+ $result2 = $this->assertQuery(
+ "INSERT INTO _tmp_table (name, lastname) VALUES ('FIRSTname', 'LASTname' );"
+ );
+
+ $this->assertEquals( 1, $result2 );
+
+ $result1 = $this->engine->query( 'SELECT COUNT(*) num FROM _tmp_table;' );
+ $this->assertEquals( 2, $result1[0]->num );
+ }
+
+ public function testOnDuplicateUpdate() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ name varchar(20) NOT NULL default '',
+ UNIQUE KEY myname (name)
+ );"
+ );
+
+ // $result1 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" );
+ // $this->assertEquals( '', $this->engine->get_error_message() );
+ // $this->assertEquals( 1, $result1 );
+
+ $result2 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST') ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);" );
+ $this->assertEquals( 1, $result2 );
+
+ $this->assertQuery( 'SELECT * FROM _tmp_table;' );
+ $this->assertCount( 1, $this->engine->get_query_results() );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'name' => 'FIRST',
+ 'ID' => 1,
+ ),
+ ),
+ $this->engine->get_query_results()
+ );
+ }
+
+ public function testTruncatesInvalidDates() {
+ $this->assertQuery( "INSERT INTO _dates (option_value) VALUES ('2022-01-01 14:24:12');" );
+ $this->assertQuery( "INSERT INTO _dates (option_value) VALUES ('2022-31-01 14:24:12');" );
+
+ $this->assertQuery( 'SELECT * FROM _dates;' );
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 2, $results );
+ $this->assertEquals( '2022-01-01 14:24:12', $results[0]->option_value );
+ $this->assertEquals( '0000-00-00 00:00:00', $results[1]->option_value );
+ }
+
+ public function testCaseInsensitiveSelect() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ name varchar(20) NOT NULL default ''
+ );"
+ );
+ $this->assertQuery(
+ "INSERT INTO _tmp_table (name) VALUES ('first');"
+ );
+ $this->assertQuery( "SELECT name FROM _tmp_table WHERE name = 'FIRST';" );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertCount( 1, $this->engine->get_query_results() );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'name' => 'first',
+ ),
+ ),
+ $this->engine->get_query_results()
+ );
+ }
+
+ public function testSelectBetweenDates() {
+ $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2016-01-15T00:00:00Z');" );
+ $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2016-01-16T00:00:00Z');" );
+ $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('third', '2016-01-17T00:00:00Z');" );
+ $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('fourth', '2016-01-18T00:00:00Z');" );
+
+ $this->assertQuery( "SELECT * FROM _dates WHERE option_value BETWEEN '2016-01-15T00:00:00Z' AND '2016-01-17T00:00:00Z' ORDER BY ID;" );
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 3, $results );
+ $this->assertEquals( 'first', $results[0]->option_name );
+ $this->assertEquals( 'second', $results[1]->option_name );
+ $this->assertEquals( 'third', $results[2]->option_name );
+ }
+
+ public function testSelectFilterByDatesGtLt() {
+ $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2016-01-15T00:00:00Z');" );
+ $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2016-01-16T00:00:00Z');" );
+ $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('third', '2016-01-17T00:00:00Z');" );
+ $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('fourth', '2016-01-18T00:00:00Z');" );
+
+ $this->assertQuery(
+ "
+ SELECT * FROM _dates
+ WHERE option_value > '2016-01-15 00:00:00'
+ AND option_value < '2016-01-17 00:00:00'
+ ORDER BY ID
+ "
+ );
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ $this->assertEquals( 'second', $results[0]->option_name );
+ }
+
+ public function testSelectFilterByDatesZeroHour() {
+ $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2014-10-21 00:42:29');" );
+ $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2014-10-21 01:42:29');" );
+
+ $this->assertQuery(
+ '
+ SELECT * FROM _dates
+ WHERE YEAR(option_value) = 2014
+ AND MONTHNUM(option_value) = 10
+ AND DAY(option_value) = 21
+ AND HOUR(option_value) = 0
+ AND MINUTE(option_value) = 42
+ '
+ );
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ $this->assertEquals( 'first', $results[0]->option_name );
+ }
+
+ public function testCorrectlyInsertsDatesAndStrings() {
+ $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('2016-01-15T00:00:00Z', '2016-01-15T00:00:00Z');" );
+
+ $this->assertQuery( 'SELECT * FROM _dates' );
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ $this->assertEquals( '2016-01-15 00:00:00', $results[0]->option_value );
+ if ( '2016-01-15T00:00:00Z' !== $results[0]->option_name ) {
+ $this->markTestSkipped( 'A datetime-like string was rewritten to an SQLite format even though it was used as a text and not as a datetime.' );
+ }
+ $this->assertEquals( '2016-01-15T00:00:00Z', $results[0]->option_name );
+ }
+
+ public function testTransactionRollback() {
+ $this->assertQuery( 'BEGIN' );
+ $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" );
+ $this->assertQuery( 'SELECT * FROM _options;' );
+ $this->assertCount( 1, $this->engine->get_query_results() );
+ $this->assertQuery( 'ROLLBACK' );
+
+ $this->assertQuery( 'SELECT * FROM _options;' );
+ $this->assertCount( 0, $this->engine->get_query_results() );
+ }
+
+ public function testTransactionCommit() {
+ $this->assertQuery( 'BEGIN' );
+ $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" );
+ $this->assertQuery( 'SELECT * FROM _options;' );
+ $this->assertCount( 1, $this->engine->get_query_results() );
+ $this->assertQuery( 'COMMIT' );
+
+ $this->assertQuery( 'SELECT * FROM _options;' );
+ $this->assertCount( 1, $this->engine->get_query_results() );
+ }
+
+ public function testStartTransactionCommand() {
+ $this->assertQuery( 'START TRANSACTION' );
+ $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" );
+ $this->assertQuery( 'SELECT * FROM _options;' );
+ $this->assertCount( 1, $this->engine->get_query_results() );
+ $this->assertQuery( 'ROLLBACK' );
+
+ $this->assertQuery( 'SELECT * FROM _options;' );
+ $this->assertCount( 0, $this->engine->get_query_results() );
+ }
+
+ public function testNestedTransactionWork() {
+ $this->assertQuery( 'BEGIN' );
+ $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" );
+ $this->assertQuery( 'START TRANSACTION' );
+ $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('second');" );
+ $this->assertQuery( 'START TRANSACTION' );
+ $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('third');" );
+ $this->assertQuery( 'SELECT * FROM _options;' );
+ $this->assertCount( 3, $this->engine->get_query_results() );
+
+ $this->assertQuery( 'ROLLBACK' );
+ $this->assertQuery( 'SELECT * FROM _options;' );
+ $this->assertCount( 2, $this->engine->get_query_results() );
+
+ $this->assertQuery( 'ROLLBACK' );
+ $this->assertQuery( 'SELECT * FROM _options;' );
+ $this->assertCount( 1, $this->engine->get_query_results() );
+
+ $this->assertQuery( 'COMMIT' );
+ $this->assertQuery( 'SELECT * FROM _options;' );
+ $this->assertCount( 1, $this->engine->get_query_results() );
+ }
+
+ public function testNestedTransactionWorkComplexModify() {
+ $this->assertQuery( 'BEGIN' );
+ // Create a complex ALTER Table query where the first
+ // column is added successfully, but the second fails.
+ // Behind the scenes, this single MySQL query is split
+ // into multiple SQLite queries – some of them will
+ // succeed, some will fail.
+ $success = $this->engine->query(
+ '
+ ALTER TABLE _options
+ ADD COLUMN test varchar(20),
+ ADD COLUMN test varchar(20)
+ '
+ );
+ $this->assertFalse( $success );
+ // Commit the transaction.
+ $this->assertQuery( 'COMMIT' );
+
+ // Confirm the entire query failed atomically and no column was
+ // added to the table.
+ $this->assertQuery( 'DESCRIBE _options;' );
+ $fields = $this->engine->get_query_results();
+
+ $this->assertEquals(
+ $fields,
+ array(
+ (object) array(
+ 'Field' => 'ID',
+ 'Type' => 'integer',
+ 'Null' => 'NO',
+ 'Key' => 'PRI',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'option_name',
+ 'Type' => 'text',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'option_value',
+ 'Type' => 'text',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '',
+ 'Extra' => '',
+ ),
+ )
+ );
+ }
+
+ public function testCount() {
+ $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('first');" );
+ $this->assertQuery( "INSERT INTO _options (option_name) VALUES ('second');" );
+ $this->assertQuery( 'SELECT COUNT(*) as count FROM _options;' );
+
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ $this->assertSame( '2', $results[0]->count );
+ }
+
+ public function testUpdateDate() {
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');"
+ );
+
+ $this->assertQuery( 'SELECT option_value FROM _dates' );
+
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ $this->assertEquals( '2003-05-27 10:08:48', $results[0]->option_value );
+
+ $this->assertQuery(
+ "UPDATE _dates SET option_value = DATE_SUB(option_value, INTERVAL '2' YEAR);"
+ );
+
+ $this->assertQuery( 'SELECT option_value FROM _dates' );
+
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ $this->assertEquals( '2001-05-27 10:08:48', $results[0]->option_value );
+ }
+
+ public function testInsertDateLiteral() {
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');"
+ );
+
+ $this->assertQuery( 'SELECT option_value FROM _dates' );
+
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ $this->assertEquals( '2003-05-27 10:08:48', $results[0]->option_value );
+ }
+
+ public function testSelectDate1() {
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2000-05-27 10:08:48');"
+ );
+
+ $this->assertQuery(
+ 'SELECT
+ YEAR( _dates.option_value ) as year,
+ MONTH( _dates.option_value ) as month,
+ DAYOFMONTH( _dates.option_value ) as dayofmonth,
+ MONTHNUM( _dates.option_value ) as monthnum,
+ WEEKDAY( _dates.option_value ) as weekday,
+ WEEK( _dates.option_value, 1 ) as week1,
+ HOUR( _dates.option_value ) as hour,
+ MINUTE( _dates.option_value ) as minute,
+ SECOND( _dates.option_value ) as second
+ FROM _dates'
+ );
+
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ $this->assertEquals( '2000', $results[0]->year );
+ $this->assertEquals( '5', $results[0]->month );
+ $this->assertEquals( '27', $results[0]->dayofmonth );
+ $this->assertEquals( '5', $results[0]->weekday );
+ $this->assertEquals( '21', $results[0]->week1 );
+ $this->assertEquals( '5', $results[0]->monthnum );
+ $this->assertEquals( '10', $results[0]->hour );
+ $this->assertEquals( '8', $results[0]->minute );
+ $this->assertEquals( '48', $results[0]->second );
+ }
+
+ public function testSelectDate24HourFormat() {
+ $this->assertQuery(
+ "
+ INSERT INTO _dates (option_name, option_value)
+ VALUES
+ ('second', '2003-05-27 14:08:48'),
+ ('first', '2003-05-27 00:08:48');
+ "
+ );
+
+ // HOUR(14:08) should yield 14 in the 24 hour format
+ $this->assertQuery( "SELECT HOUR( _dates.option_value ) as hour FROM _dates WHERE option_name = 'second'" );
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ $this->assertEquals( '14', $results[0]->hour );
+
+ // HOUR(00:08) should yield 0 in the 24 hour format
+ $this->assertQuery( "SELECT HOUR( _dates.option_value ) as hour FROM _dates WHERE option_name = 'first'" );
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ $this->assertEquals( '0', $results[0]->hour );
+
+ // Lookup by HOUR(00:08) = 0 should yield the right record
+ $this->assertQuery(
+ 'SELECT HOUR( _dates.option_value ) as hour FROM _dates
+ WHERE HOUR(_dates.option_value) = 0 '
+ );
+
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ $this->assertEquals( '0', $results[0]->hour );
+ }
+
+ public function testSelectByDateFunctions() {
+ $this->assertQuery(
+ "
+ INSERT INTO _dates (option_name, option_value)
+ VALUES ('second', '2014-10-21 00:42:29');
+ "
+ );
+
+ // HOUR(14:08) should yield 14 in the 24 hour format
+ $this->assertQuery(
+ '
+ SELECT * FROM _dates WHERE
+ year(option_value) = 2014
+ AND monthnum(option_value) = 10
+ AND day(option_value) = 21
+ AND hour(option_value) = 0
+ AND minute(option_value) = 42
+ '
+ );
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ }
+
+ public function testSelectByDateFormat() {
+ $this->assertQuery(
+ "
+ INSERT INTO _dates (option_name, option_value)
+ VALUES ('second', '2014-10-21 00:42:29');
+ "
+ );
+
+ // HOUR(14:08) should yield 14 in the 24 hour format
+ $this->assertQuery(
+ "
+ SELECT * FROM _dates WHERE DATE_FORMAT(option_value, '%H.%i') = 0.42
+ "
+ );
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ }
+
+ public function testInsertOnDuplicateKey() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ name varchar(20) NOT NULL default '',
+ UNIQUE KEY name (name)
+ );"
+ );
+ $result1 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" );
+ $this->assertEquals( 1, $result1 );
+
+ $result2 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST') ON DUPLICATE KEY SET name=VALUES(`name`);" );
+ $this->assertEquals( 1, $result2 );
+
+ $this->assertQuery( 'SELECT COUNT(*) as cnt FROM _tmp_table' );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals( 1, $results[0]->cnt );
+ }
+
+ public function testCreateTableCompositePk() {
+ $this->assertQuery(
+ 'CREATE TABLE wptests_term_relationships (
+ object_id bigint(20) unsigned NOT NULL default 0,
+ term_taxonomy_id bigint(20) unsigned NOT NULL default 0,
+ term_order int(11) NOT NULL default 0,
+ PRIMARY KEY (object_id,term_taxonomy_id),
+ KEY term_taxonomy_id (term_taxonomy_id)
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci'
+ );
+ $result1 = $this->engine->query( 'INSERT INTO wptests_term_relationships VALUES (1,2,1),(1,3,2);' );
+ $this->assertEquals( 2, $result1 );
+
+ $result2 = $this->engine->query( 'INSERT INTO wptests_term_relationships VALUES (1,2,2),(1,3,1);' );
+ $this->assertEquals( false, $result2 );
+ }
+
+ public function testDescribeAccurate() {
+ $result = $this->assertQuery(
+ 'CREATE TABLE wptests_term_relationships (
+ object_id bigint(20) unsigned NOT NULL default 0,
+ term_taxonomy_id bigint(20) unsigned NOT NULL default 0,
+ term_name varchar(11) NOT NULL default 0,
+ PRIMARY KEY (object_id,term_taxonomy_id),
+ KEY term_taxonomy_id (term_taxonomy_id),
+ KEY compound_key (object_id(20),term_taxonomy_id(20)),
+ FULLTEXT KEY term_name (term_name)
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci'
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertNotFalse( $result );
+
+ $result = $this->assertQuery( 'DESCRIBE wptests_term_relationships;' );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertNotFalse( $result );
+
+ $fields = $this->engine->get_query_results();
+
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'object_id',
+ 'Type' => 'bigint(20) unsigned',
+ 'Null' => 'NO',
+ 'Key' => 'PRI',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'term_taxonomy_id',
+ 'Type' => 'bigint(20) unsigned',
+ 'Null' => 'NO',
+ 'Key' => 'PRI',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'term_name',
+ 'Type' => 'varchar(11)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ ),
+ $fields
+ );
+ }
+
+ public function testAlterTableAddColumnChangesMySQLDataType() {
+ $result = $this->assertQuery(
+ 'CREATE TABLE _test (
+ object_id bigint(20) unsigned NOT NULL default 0
+ )'
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertNotFalse( $result );
+
+ $result = $this->assertQuery( "ALTER TABLE `_test` ADD COLUMN object_name varchar(255) NOT NULL DEFAULT 'adb';" );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertNotFalse( $result );
+
+ $result = $this->assertQuery( 'DESCRIBE _test;' );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertNotFalse( $result );
+ $fields = $this->engine->get_query_results();
+
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'object_id',
+ 'Type' => 'bigint(20) unsigned',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => '0',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'object_name',
+ 'Type' => 'varchar(255)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => 'adb',
+ 'Extra' => '',
+ ),
+ ),
+ $fields
+ );
+ }
+ public function testShowGrantsFor() {
+ $result = $this->assertQuery( 'SHOW GRANTS FOR current_user();' );
+ $this->assertEquals(
+ $result,
+ array(
+ (object) array(
+ 'Grants for root@localhost' => 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON *.* TO `root`@`localhost` WITH GRANT OPTION',
+ ),
+ )
+ );
+ }
+
+ public function testShowIndex() {
+ $result = $this->assertQuery(
+ 'CREATE TABLE wptests_term_relationships (
+ object_id bigint(20) unsigned NOT NULL default 0,
+ term_taxonomy_id bigint(20) unsigned NOT NULL default 0,
+ term_name varchar(11) NOT NULL default 0,
+ FULLTEXT KEY term_name_fulltext (term_name),
+ FULLTEXT INDEX term_name_fulltext2 (`term_name`),
+ SPATIAL KEY term_name_spatial (term_name),
+ PRIMARY KEY (object_id,term_taxonomy_id),
+ KEY term_taxonomy_id (term_taxonomy_id),
+ KEY compound_key (object_id(20),term_taxonomy_id(20))
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci'
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertNotFalse( $result );
+
+ $result = $this->assertQuery( 'SHOW INDEX FROM wptests_term_relationships;' );
+ $this->assertNotFalse( $result );
+
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Table' => 'wptests_term_relationships',
+ 'Non_unique' => '0',
+ 'Key_name' => 'PRIMARY',
+ 'Seq_in_index' => '0',
+ 'Column_name' => 'object_id',
+ 'Collation' => 'A',
+ 'Cardinality' => '0',
+ 'Sub_part' => null,
+ 'Packed' => null,
+ 'Null' => '',
+ 'Index_type' => 'BTREE',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ (object) array(
+ 'Table' => 'wptests_term_relationships',
+ 'Non_unique' => '0',
+ 'Key_name' => 'PRIMARY',
+ 'Seq_in_index' => '0',
+ 'Column_name' => 'term_taxonomy_id',
+ 'Collation' => 'A',
+ 'Cardinality' => '0',
+ 'Sub_part' => null,
+ 'Packed' => null,
+ 'Null' => '',
+ 'Index_type' => 'BTREE',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ (object) array(
+ 'Table' => 'wptests_term_relationships',
+ 'Non_unique' => '1',
+ 'Key_name' => 'compound_key',
+ 'Seq_in_index' => '0',
+ 'Column_name' => 'object_id',
+ 'Collation' => 'A',
+ 'Cardinality' => '0',
+ 'Sub_part' => null,
+ 'Packed' => null,
+ 'Null' => '',
+ 'Index_type' => 'BTREE',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ (object) array(
+ 'Table' => 'wptests_term_relationships',
+ 'Non_unique' => '1',
+ 'Key_name' => 'compound_key',
+ 'Seq_in_index' => '0',
+ 'Column_name' => 'term_taxonomy_id',
+ 'Collation' => 'A',
+ 'Cardinality' => '0',
+ 'Sub_part' => null,
+ 'Packed' => null,
+ 'Null' => '',
+ 'Index_type' => 'BTREE',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ (object) array(
+ 'Table' => 'wptests_term_relationships',
+ 'Non_unique' => '1',
+ 'Key_name' => 'term_taxonomy_id',
+ 'Seq_in_index' => '0',
+ 'Column_name' => 'term_taxonomy_id',
+ 'Collation' => 'A',
+ 'Cardinality' => '0',
+ 'Sub_part' => null,
+ 'Packed' => null,
+ 'Null' => '',
+ 'Index_type' => 'BTREE',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ (object) array(
+ 'Table' => 'wptests_term_relationships',
+ 'Non_unique' => '1',
+ 'Key_name' => 'term_name_spatial',
+ 'Seq_in_index' => '0',
+ 'Column_name' => 'term_name',
+ 'Collation' => 'A',
+ 'Cardinality' => '0',
+ 'Sub_part' => null,
+ 'Packed' => null,
+ 'Null' => '',
+ 'Index_type' => 'SPATIAL',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ (object) array(
+ 'Table' => 'wptests_term_relationships',
+ 'Non_unique' => '1',
+ 'Key_name' => 'term_name_fulltext2',
+ 'Seq_in_index' => '0',
+ 'Column_name' => 'term_name',
+ 'Collation' => 'A',
+ 'Cardinality' => '0',
+ 'Sub_part' => null,
+ 'Packed' => null,
+ 'Null' => '',
+ 'Index_type' => 'FULLTEXT',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ (object) array(
+ 'Table' => 'wptests_term_relationships',
+ 'Non_unique' => '1',
+ 'Key_name' => 'term_name_fulltext',
+ 'Seq_in_index' => '0',
+ 'Column_name' => 'term_name',
+ 'Collation' => 'A',
+ 'Cardinality' => '0',
+ 'Sub_part' => null,
+ 'Packed' => null,
+ 'Null' => '',
+ 'Index_type' => 'FULLTEXT',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ (object) array(
+ 'Table' => 'wptests_term_relationships',
+ 'Non_unique' => '0',
+ 'Key_name' => 'wptests_term_relationships',
+ 'Seq_in_index' => '0',
+ 'Column_name' => 'object_id',
+ 'Collation' => 'A',
+ 'Cardinality' => '0',
+ 'Sub_part' => null,
+ 'Packed' => null,
+ 'Null' => '',
+ 'Index_type' => 'BTREE',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ (object) array(
+ 'Table' => 'wptests_term_relationships',
+ 'Non_unique' => '0',
+ 'Key_name' => 'wptests_term_relationships',
+ 'Seq_in_index' => '0',
+ 'Column_name' => 'term_taxonomy_id',
+ 'Collation' => 'A',
+ 'Cardinality' => '0',
+ 'Sub_part' => null,
+ 'Packed' => null,
+ 'Null' => '',
+ 'Index_type' => 'BTREE',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ ),
+ $this->engine->get_query_results()
+ );
+ }
+
+ public function testInsertOnDuplicateKeyCompositePk() {
+ $result = $this->assertQuery(
+ 'CREATE TABLE wptests_term_relationships (
+ object_id bigint(20) unsigned NOT NULL default 0,
+ term_taxonomy_id bigint(20) unsigned NOT NULL default 0,
+ term_order int(11) NOT NULL default 0,
+ PRIMARY KEY (object_id,term_taxonomy_id),
+ KEY term_taxonomy_id (term_taxonomy_id)
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci'
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertNotFalse( $result );
+
+ $result1 = $this->assertQuery( 'INSERT INTO wptests_term_relationships VALUES (1,2,1),(1,3,2);' );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 2, $result1 );
+
+ $result2 = $this->assertQuery( 'INSERT INTO wptests_term_relationships VALUES (1,2,2),(1,3,1) ON DUPLICATE KEY SET term_order = VALUES(term_order);' );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 2, $result2 );
+
+ $this->assertQuery( 'SELECT COUNT(*) as cnt FROM wptests_term_relationships' );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals( 2, $results[0]->cnt );
+ }
+
+ public function testStringToFloatComparison() {
+ $this->assertQuery( "SELECT ('00.42' = 0.4200) as cmp;" );
+ $results = $this->engine->get_query_results();
+ if ( 1 !== $results[0]->cmp ) {
+ $this->markTestSkipped( 'Comparing a string and a float returns true in MySQL. In SQLite, they\'re different. Skipping. ' );
+ }
+ $this->assertEquals( '1', $results[0]->cmp );
+
+ $this->assertQuery( "SELECT (0+'00.42' = 0.4200) as cmp;" );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals( '1', $results[0]->cmp );
+ }
+
+ public function testZeroPlusStringToFloatComparison() {
+
+ $this->assertQuery( "SELECT (0+'00.42' = 0.4200) as cmp;" );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals( '1', $results[0]->cmp );
+
+ $this->assertQuery( "SELECT 0+'1234abcd' = 1234 as cmp;" );
+ $results = $this->engine->get_query_results();
+ $this->assertEquals( '1', $results[0]->cmp );
+ }
+
+ public function testCalcFoundRows() {
+ $result = $this->assertQuery(
+ "CREATE TABLE wptests_dummy (
+ ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ user_login TEXT NOT NULL default ''
+ );"
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertNotFalse( $result );
+
+ $result = $this->assertQuery(
+ "INSERT INTO wptests_dummy (user_login) VALUES ('test');"
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $result = $this->assertQuery(
+ 'SELECT SQL_CALC_FOUND_ROWS * FROM wptests_dummy'
+ );
+ $this->assertNotFalse( $result );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 'test', $result[0]->user_login );
+ }
+
+ public function testComplexSelectBasedOnDates() {
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');"
+ );
+
+ $this->assertQuery(
+ 'SELECT SQL_CALC_FOUND_ROWS _dates.ID
+ FROM _dates
+ WHERE YEAR( _dates.option_value ) = 2003 AND MONTH( _dates.option_value ) = 5 AND DAYOFMONTH( _dates.option_value ) = 27
+ ORDER BY _dates.option_value DESC
+ LIMIT 0, 10'
+ );
+
+ $results = $this->engine->get_query_results();
+ $this->assertCount( 1, $results );
+ }
+
+ public function testUpdateReturnValue() {
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');"
+ );
+
+ $return = $this->assertQuery(
+ "UPDATE _dates SET option_value = '2001-05-27 10:08:48'"
+ );
+ $this->assertSame( 1, $return, 'UPDATE query did not return 1 when one row was changed' );
+
+ $return = $this->assertQuery(
+ "UPDATE _dates SET option_value = '2001-05-27 10:08:48'"
+ );
+ if ( 1 === $return ) {
+ $this->markTestIncomplete(
+ 'SQLite UPDATE query returned 1 when no rows were changed. ' .
+ 'This is a database compatibility issue – MySQL would return 0 ' .
+ 'in the same scenario.'
+ );
+ }
+ $this->assertSame( 0, $return, 'UPDATE query did not return 0 when no rows were changed' );
+ }
+
+ public function testOrderByField() {
+ $this->assertQuery(
+ "INSERT INTO _options (option_name, option_value) VALUES ('User 0000019', 'second');"
+ );
+ $this->assertQuery(
+ "INSERT INTO _options (option_name, option_value) VALUES ('User 0000020', 'third');"
+ );
+ $this->assertQuery(
+ "INSERT INTO _options (option_name, option_value) VALUES ('User 0000018', 'first');"
+ );
+
+ $this->assertQuery( 'SELECT FIELD(option_name, "User 0000018", "User 0000019", "User 0000020") as sorting_order FROM _options ORDER BY FIELD(option_name, "User 0000018", "User 0000019", "User 0000020")' );
+
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'sorting_order' => '1',
+ ),
+ (object) array(
+ 'sorting_order' => '2',
+ ),
+ (object) array(
+ 'sorting_order' => '3',
+ ),
+ ),
+ $this->engine->get_query_results()
+ );
+
+ $this->assertQuery( 'SELECT option_value FROM _options ORDER BY FIELD(option_name, "User 0000018", "User 0000019", "User 0000020")' );
+
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'option_value' => 'first',
+ ),
+ (object) array(
+ 'option_value' => 'second',
+ ),
+ (object) array(
+ 'option_value' => 'third',
+ ),
+ ),
+ $this->engine->get_query_results()
+ );
+ }
+
+ public function testFetchedDataIsStringified() {
+ $this->assertQuery(
+ "INSERT INTO _options (option_name, option_value) VALUES ('rss_0123456789abcdef0123456789abcdef', '1');"
+ );
+
+ $this->assertQuery( 'SELECT ID FROM _options' );
+
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'ID' => '1',
+ ),
+ ),
+ $this->engine->get_query_results()
+ );
+ }
+
+ public function testCreateTableQuery() {
+ $this->assertQuery(
+ <<<'QUERY'
+ CREATE TABLE IF NOT EXISTS wptests_users (
+ ID bigint(20) unsigned NOT NULL auto_increment,
+ user_login varchar(60) NOT NULL default '',
+ user_pass varchar(255) NOT NULL default '',
+ user_nicename varchar(50) NOT NULL default '',
+ user_email varchar(100) NOT NULL default '',
+ user_url varchar(100) NOT NULL default '',
+ user_registered datetime NOT NULL default '0000-00-00 00:00:00',
+ user_activation_key varchar(255) NOT NULL default '',
+ user_status int(11) NOT NULL default '0',
+ display_name varchar(250) NOT NULL default '',
+ PRIMARY KEY (ID),
+ KEY user_login_key (user_login),
+ KEY user_nicename (user_nicename),
+ KEY user_email (user_email)
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci
+QUERY
+ );
+ $this->assertQuery(
+ <<<'QUERY'
+ INSERT INTO wptests_users VALUES (1,'admin','$P$B5ZQZ5ZQZ5ZQZ5ZQZ5ZQZ5ZQZ5ZQZ5','admin','admin@localhost', '', '2019-01-01 00:00:00', '', 0, 'admin');
+QUERY
+ );
+ $rows = $this->assertQuery( 'SELECT * FROM wptests_users' );
+ $this->assertCount( 1, $rows );
+
+ $this->assertQuery( 'SELECT SQL_CALC_FOUND_ROWS * FROM wptests_users' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'FOUND_ROWS()' => '1',
+ ),
+ ),
+ $result
+ );
+ }
+
+ public function testTranslatesComplexDelete() {
+ $this->sqlite->query(
+ "CREATE TABLE wptests_dummy (
+ ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ user_login TEXT NOT NULL default '',
+ option_name TEXT NOT NULL default '',
+ option_value TEXT NOT NULL default ''
+ );"
+ );
+ $this->sqlite->query(
+ "INSERT INTO wptests_dummy (user_login, option_name, option_value) VALUES ('admin', '_transient_timeout_test', '1675963960');"
+ );
+ $this->sqlite->query(
+ "INSERT INTO wptests_dummy (user_login, option_name, option_value) VALUES ('admin', '_transient_test', '1675963960');"
+ );
+
+ $result = $this->assertQuery(
+ "DELETE a, b FROM wptests_dummy a, wptests_dummy b
+ WHERE a.option_name LIKE '\_transient\_%'
+ AND a.option_name NOT LIKE '\_transient\_timeout_%'
+ AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) );"
+ );
+ $this->assertEquals(
+ 2,
+ $result
+ );
+ }
+
+ public function testTranslatesDoubleAlterTable() {
+ $result = $this->assertQuery(
+ 'ALTER TABLE _options
+ ADD INDEX test_index(option_name(140),option_value(51)),
+ DROP INDEX test_index,
+ ADD INDEX test_index2(option_name(140),option_value(51))
+ '
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals(
+ 1,
+ $result
+ );
+ $result = $this->assertQuery(
+ 'SHOW INDEX FROM _options'
+ );
+ $this->assertCount( 3, $result );
+ $this->assertEquals( 'PRIMARY', $result[0]->Key_name );
+ $this->assertEquals( 'test_index2', $result[1]->Key_name );
+ $this->assertEquals( 'test_index2', $result[2]->Key_name );
+ }
+
+ public function testTranslatesComplexSelect() {
+ $this->assertQuery(
+ "CREATE TABLE wptests_postmeta (
+ meta_id bigint(20) unsigned NOT NULL auto_increment,
+ post_id bigint(20) unsigned NOT NULL default '0',
+ meta_key varchar(255) default NULL,
+ meta_value longtext,
+ PRIMARY KEY (meta_id),
+ KEY post_id (post_id),
+ KEY meta_key (meta_key(191))
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci"
+ );
+ $this->assertQuery(
+ "CREATE TABLE wptests_posts (
+ ID bigint(20) unsigned NOT NULL auto_increment,
+ post_status varchar(20) NOT NULL default 'open',
+ post_type varchar(20) NOT NULL default 'post',
+ post_date varchar(20) NOT NULL default 'post',
+ PRIMARY KEY (ID)
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci"
+ );
+ $result = $this->assertQuery(
+ "SELECT SQL_CALC_FOUND_ROWS wptests_posts.ID
+ FROM wptests_posts INNER JOIN wptests_postmeta ON ( wptests_posts.ID = wptests_postmeta.post_id )
+ WHERE 1=1
+ AND (
+ NOT EXISTS (
+ SELECT 1 FROM wptests_postmeta mt1
+ WHERE mt1.post_ID = wptests_postmeta.post_ID
+ LIMIT 1
+ )
+ )
+ AND (
+ (wptests_posts.post_type = 'post' AND (wptests_posts.post_status = 'publish'))
+ )
+ GROUP BY wptests_posts.ID
+ ORDER BY wptests_posts.post_date DESC
+ LIMIT 0, 10"
+ );
+
+ // No exception is good enough of a test for now
+ $this->assertTrue( true );
+ }
+
+ public function testTranslatesUtf8Insert() {
+ $this->assertQuery(
+ "INSERT INTO _options VALUES(1,'ąłółźćę†','ąłółźćę†')"
+ );
+ $this->assertCount(
+ 1,
+ $this->assertQuery( 'SELECT * FROM _options' )
+ );
+ $this->assertQuery( 'DELETE FROM _options' );
+ }
+
+ public function testTranslatesRandom() {
+ $this->assertIsNumeric(
+ $this->sqlite->query( 'SELECT RAND() AS rand' )->fetchColumn()
+ );
+
+ $this->assertIsNumeric(
+ $this->sqlite->query( 'SELECT RAND(5) AS rand' )->fetchColumn()
+ );
+ }
+
+ public function testTranslatesUtf8SELECT() {
+ $this->assertQuery(
+ "INSERT INTO _options VALUES(1,'ąłółźćę†','ąłółźćę†')"
+ );
+ $this->assertCount(
+ 1,
+ $this->assertQuery( 'SELECT * FROM _options' )
+ );
+
+ $this->assertQuery(
+ "SELECT option_name as 'ą' FROM _options WHERE option_name='ąłółźćę†' AND option_value='ąłółźćę†'"
+ );
+
+ $this->assertEquals(
+ array( (object) array( 'ą' => 'ąłółźćę†' ) ),
+ $this->engine->get_query_results()
+ );
+
+ $this->assertQuery(
+ "SELECT option_name as 'ą' FROM _options WHERE option_name LIKE '%ółźć%'"
+ );
+
+ $this->assertEquals(
+ array( (object) array( 'ą' => 'ąłółźćę†' ) ),
+ $this->engine->get_query_results()
+ );
+
+ $this->assertQuery( 'DELETE FROM _options' );
+ }
+
+ public function testTranslateLikeBinaryAndGlob() {
+ // Create a temporary table for testing
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ name varchar(20) NOT NULL default ''
+ );"
+ );
+
+ // Insert data into the table
+ $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" );
+ $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST');" );
+ $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('second');" );
+ $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('');" );
+ $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('%special%');" );
+ $this->assertQuery( 'INSERT INTO _tmp_table (name) VALUES (NULL);' );
+ $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special%chars');" );
+ $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special_chars');" );
+ $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special\\chars');" );
+
+ // Test case-sensitive LIKE BINARY
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'first'" );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 'first', $result[0]->name );
+
+ // Test case-sensitive LIKE BINARY with wildcard %
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'f%'" );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 'first', $result[0]->name );
+
+ // Test case-sensitive LIKE BINARY with wildcard _
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'f_rst'" );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 'first', $result[0]->name );
+
+ // Test case-insensitive LIKE
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE 'FIRST'" );
+ $this->assertCount( 2, $result ); // Should match both 'first' and 'FIRST'
+
+ // Test mixed case with LIKE BINARY
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'First'" );
+ $this->assertCount( 0, $result );
+
+ // Test no matches with LIKE BINARY
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'third'" );
+ $this->assertCount( 0, $result );
+
+ // Test GLOB equivalent for case-sensitive matching with wildcard
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'f*'" );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 'first', $result[0]->name );
+
+ // Test GLOB with single character wildcard
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'f?rst'" );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 'first', $result[0]->name );
+
+ // Test GLOB with no matches
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'S*'" );
+ $this->assertCount( 0, $result );
+
+ // Test GLOB case sensitivity with LIKE and GLOB
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'first';" );
+ $this->assertCount( 1, $result ); // Should only match 'first'
+
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'FIRST';" );
+ $this->assertCount( 1, $result ); // Should only match 'FIRST'
+
+ // Test NULL comparison with LIKE BINARY
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'first';" );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 'first', $result[0]->name );
+
+ $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE name LIKE BINARY NULL;' );
+ $this->assertCount( 0, $result ); // NULL comparison should return no results
+
+ // Test pattern with special characters using LIKE BINARY
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY '%special%';" );
+ $this->assertCount( 4, $result );
+ $this->assertEquals( '%special%', $result[0]->name );
+ $this->assertEquals( 'special%chars', $result[1]->name );
+ $this->assertEquals( 'special_chars', $result[2]->name );
+ $this->assertEquals( 'specialchars', $result[3]->name );
+ }
+
+ public function testOnConflictReplace() {
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table (
+ ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ name varchar(20) NOT NULL default 'default-value',
+ unique_name varchar(20) NOT NULL default 'unique-default-value',
+ inline_unique_name varchar(20) NOT NULL default 'inline-unique-default-value',
+ no_default varchar(20) NOT NULL,
+ UNIQUE KEY unique_name (unique_name)
+ );"
+ );
+
+ $this->assertQuery(
+ "INSERT INTO _tmp_table VALUES (1, null, null, null, '');"
+ );
+ $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE ID = 1' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'ID' => '1',
+ 'name' => 'default-value',
+ 'unique_name' => 'unique-default-value',
+ 'inline_unique_name' => 'inline-unique-default-value',
+ 'no_default' => '',
+ ),
+ ),
+ $result
+ );
+
+ $this->assertQuery(
+ "INSERT INTO _tmp_table VALUES (2, '1', '2', '3', '4');"
+ );
+ $this->assertQuery(
+ 'UPDATE _tmp_table SET name = null WHERE ID = 2;'
+ );
+
+ $result = $this->assertQuery( 'SELECT name FROM _tmp_table WHERE ID = 2' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'name' => 'default-value',
+ ),
+ ),
+ $result
+ );
+
+ // This should fail because of the UNIQUE constraint
+ $this->assertQuery(
+ 'UPDATE _tmp_table SET unique_name = NULL WHERE ID = 2;',
+ 'UNIQUE constraint failed: _tmp_table.unique_name'
+ );
+
+ // Inline unique constraint aren't supported currently, so this should pass
+ $this->assertQuery(
+ 'UPDATE _tmp_table SET inline_unique_name = NULL WHERE ID = 2;',
+ ''
+ );
+
+ // WPDB allows for NULL values in columns that don't have a default value and a NOT NULL constraint
+ $this->assertQuery(
+ 'UPDATE _tmp_table SET no_default = NULL WHERE ID = 2;',
+ ''
+ );
+
+ $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE ID = 2' );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'ID' => '2',
+ 'name' => 'default-value',
+ 'unique_name' => '2',
+ 'inline_unique_name' => 'inline-unique-default-value',
+ 'no_default' => '',
+ ),
+ ),
+ $result
+ );
+ }
+
+ public function testDefaultNullValue() {
+ $this->assertQuery(
+ 'CREATE TABLE _tmp_table (
+ name varchar(20) NOT NULL default NULL,
+ no_default varchar(20) NOT NULL
+ );'
+ );
+
+ $result = $this->assertQuery(
+ 'DESCRIBE _tmp_table;'
+ );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Field' => 'name',
+ 'Type' => 'varchar(20)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => 'NULL',
+ 'Extra' => '',
+ ),
+ (object) array(
+ 'Field' => 'no_default',
+ 'Type' => 'varchar(20)',
+ 'Null' => 'NO',
+ 'Key' => '',
+ 'Default' => null,
+ 'Extra' => '',
+ ),
+ ),
+ $result
+ );
+ }
+
+ public function testCurrentTimestamp() {
+ // SELECT
+ $results = $this->assertQuery(
+ 'SELECT
+ current_timestamp AS t1,
+ CURRENT_TIMESTAMP AS t2,
+ current_timestamp() AS t3,
+ CURRENT_TIMESTAMP() AS t4'
+ );
+ $this->assertIsArray( $results );
+ $this->assertCount( 1, $results );
+ $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t1 );
+ $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t2 );
+ $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t3 );
+
+ // INSERT
+ $this->assertQuery(
+ "INSERT INTO _dates (option_name, option_value) VALUES ('first', CURRENT_TIMESTAMP())"
+ );
+ $results = $this->assertQuery( 'SELECT option_value AS t FROM _dates' );
+ $this->assertCount( 1, $results );
+ $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t );
+
+ // UPDATE
+ $this->assertQuery( 'UPDATE _dates SET option_value = NULL' );
+ $results = $this->assertQuery( 'SELECT option_value AS t FROM _dates' );
+ $this->assertCount( 1, $results );
+ $this->assertEmpty( $results[0]->t );
+
+ $this->assertQuery( 'UPDATE _dates SET option_value = CURRENT_TIMESTAMP()' );
+ $results = $this->assertQuery( 'SELECT option_value AS t FROM _dates' );
+ $this->assertCount( 1, $results );
+ $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t );
+
+ // DELETE
+ // We can only assert that the query passes. It is not guaranteed that we'll actually
+ // delete the existing record, as the delete query could fall into a different second.
+ $this->assertQuery( 'DELETE FROM _dates WHERE option_value = CURRENT_TIMESTAMP()' );
+ }
+
+ public function testGroupByHaving() {
+ $this->assertQuery(
+ 'CREATE TABLE _tmp_table (
+ name varchar(20)
+ );'
+ );
+
+ $this->assertQuery(
+ "INSERT INTO _tmp_table VALUES ('a'), ('b'), ('b'), ('c'), ('c'), ('c')"
+ );
+
+ $result = $this->assertQuery(
+ 'SELECT name, COUNT(*) as count FROM _tmp_table GROUP BY name HAVING COUNT(*) > 1'
+ );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'name' => 'b',
+ 'count' => '2',
+ ),
+ (object) array(
+ 'name' => 'c',
+ 'count' => '3',
+ ),
+ ),
+ $result
+ );
+ }
+
+ public function testHavingWithoutGroupBy() {
+ $this->assertQuery(
+ 'CREATE TABLE _tmp_table (
+ name varchar(20)
+ );'
+ );
+
+ $this->assertQuery(
+ "INSERT INTO _tmp_table VALUES ('a'), ('b'), ('b'), ('c'), ('c'), ('c')"
+ );
+
+ // HAVING condition satisfied
+ $result = $this->assertQuery(
+ "SELECT 'T' FROM _tmp_table HAVING COUNT(*) > 1"
+ );
+ $this->assertEquals(
+ array(
+ (object) array(
+ ':param0' => 'T',
+ ),
+ ),
+ $result
+ );
+
+ // HAVING condition not satisfied
+ $result = $this->assertQuery(
+ "SELECT 'T' FROM _tmp_table HAVING COUNT(*) > 100"
+ );
+ $this->assertEquals(
+ array(),
+ $result
+ );
+
+ // DISTINCT ... HAVING, where only some results meet the HAVING condition
+ $result = $this->assertQuery(
+ 'SELECT DISTINCT name FROM _tmp_table HAVING COUNT(*) > 1'
+ );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'name' => 'b',
+ ),
+ (object) array(
+ 'name' => 'c',
+ ),
+ ),
+ $result
+ );
+ }
+
+ /**
+ * @dataProvider mysqlVariablesToTest
+ */
+ public function testSelectVariable( $variable_name ) {
+ // Make sure the query does not error
+ $this->assertQuery( "SELECT $variable_name;" );
+ }
+
+ public static function mysqlVariablesToTest() {
+ return array(
+ // NOTE: This list was derived from the variables used by the UpdraftPlus plugin.
+ // We will start here and plan to expand supported variables over time.
+ array( '@@character_set_client' ),
+ array( '@@character_set_results' ),
+ array( '@@collation_connection' ),
+ array( '@@GLOBAL.gtid_purged' ),
+ array( '@@GLOBAL.log_bin' ),
+ array( '@@GLOBAL.log_bin_trust_function_creators' ),
+ array( '@@GLOBAL.sql_mode' ),
+ array( '@@SESSION.max_allowed_packet' ),
+ array( '@@SESSION.sql_mode' ),
+
+ // Intentionally mix letter casing to help demonstrate case-insensitivity
+ array( '@@cHarActer_Set_cLient' ),
+ array( '@@gLoBAL.gTiD_purGed' ),
+ array( '@@sEssIOn.sqL_moDe' ),
+ );
+ }
+}
diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php
index a4594bd..555b454 100644
--- a/tests/tools/dump-sqlite-query.php
+++ b/tests/tools/dump-sqlite-query.php
@@ -14,10 +14,8 @@
use WIP\WP_SQLite_Driver;
-$grammar_data = include __DIR__ . '/../../wp-includes/mysql/mysql-grammar.php';
-$grammar = new WP_Parser_Grammar( $grammar_data );
-$driver = new WP_SQLite_Driver( $grammar );
+$driver = new WP_SQLite_Driver( new PDO( 'sqlite::memory:' ) );
$query = "SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id WHERE t1.name = 'abc'";
-echo $driver->run_query( $query );
+echo $driver->query( $query );
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 5ada962..cd7e36f 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -11,17 +11,36 @@
use WP_Parser_Grammar;
use WP_Parser_Node;
+$grammar = new WP_Parser_Grammar( require __DIR__ . '/../../wp-includes/mysql/mysql-grammar.php' );
+
class WP_SQLite_Driver {
+ /**
+ * @var WP_Parser_Grammar
+ */
private $grammar;
+
+ /**
+ * @var PDO
+ */
+ private $pdo;
+
+ private $results;
+
private $has_sql_calc_found_rows = false;
private $has_found_rows_call = false;
private $last_calc_rows_result = null;
- public function __construct( $grammar ) {
+ public function __construct( PDO $pdo ) {
+ global $grammar;
+ $this->pdo = $pdo;
$this->grammar = $grammar;
+
+ $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
+ $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
+ $pdo->setAttribute( PDO::ATTR_TIMEOUT, 5 );
}
- public function run_query( $query ) {
+ public function query( $query ) {
$this->has_sql_calc_found_rows = false;
$this->has_found_rows_call = false;
$this->last_calc_rows_result = null;
@@ -32,13 +51,37 @@ public function run_query( $query ) {
$parser = new WP_MySQL_Parser( $this->grammar, $tokens );
$ast = $parser->parse();
$expr = $this->translate_query( $ast );
- $expr = $this->rewrite_sql_calc_found_rows( $expr );
+ //$expr = $this->rewrite_sql_calc_found_rows( $expr );
+
+ if ( null === $expr ) {
+ return false;
+ }
$sqlite_query = WP_SQLite_Query_Builder::stringify( $expr );
// Returning the query just for now for testing. In the end, we'll
// run it and return the SQLite interaction result.
- return $sqlite_query;
+ //return $sqlite_query;
+
+ if ( ! $sqlite_query ) {
+ return false;
+ }
+
+ $is_select = (bool) $ast->get_descendant( 'selectStatement' );
+ $statement = $this->pdo->prepare( $sqlite_query );
+ $return_value = $statement->execute();
+ $this->results = $return_value;
+ if ( $is_select ) {
+ $this->results = $statement->fetchAll( PDO::FETCH_OBJ );
+ }
+ return $return_value;
+ }
+
+ public function get_error_message() {
+ }
+
+ public function get_query_results() {
+ return $this->results;
}
private function rewrite_sql_calc_found_rows( WP_SQLite_Expression $expr ) {
@@ -252,6 +295,7 @@ private function translate_query( $ast ) {
return $this->translate_runtime_function_call( $ast );
default:
+ return null;
// var_dump(count($ast->children));
// foreach($ast->children as $child) {
// var_dump(get_class($child));
From c8ea8557cc9e64099043250ee1004f6efdc6b21f Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Mon, 18 Nov 2024 21:16:47 +0100
Subject: [PATCH 004/124] Copy the core of WP_SQLite_Translator to
WP_SQLite_Driver
Additionally, move the current SQLite driver prototype to a temporary class
WP_SQLite_Driver_Prototype to be gradually moved to the new driver.
---
tests/tools/dump-sqlite-query.php | 1 +
.../class-wp-sqlite-driver-prototype.php | 670 +++++++++
.../sqlite-ast/class-wp-sqlite-driver.php | 1298 +++++++++--------
3 files changed, 1378 insertions(+), 591 deletions(-)
create mode 100644 wp-includes/sqlite-ast/class-wp-sqlite-driver-prototype.php
diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php
index 555b454..8b2ada8 100644
--- a/tests/tools/dump-sqlite-query.php
+++ b/tests/tools/dump-sqlite-query.php
@@ -6,6 +6,7 @@
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php';
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php';
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php';
+require_once __DIR__ . '/../../wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php';
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-expression.php';
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-token-factory.php';
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver-prototype.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver-prototype.php
new file mode 100644
index 0000000..98e732a
--- /dev/null
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver-prototype.php
@@ -0,0 +1,670 @@
+pdo = $pdo;
+ $this->grammar = $grammar;
+
+ $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
+ $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
+ $pdo->setAttribute( PDO::ATTR_TIMEOUT, 5 );
+ }
+
+ public function query( $query ) {
+ $this->has_sql_calc_found_rows = false;
+ $this->has_found_rows_call = false;
+ $this->last_calc_rows_result = null;
+
+ $lexer = new WP_MySQL_Lexer( $query );
+ $tokens = $lexer->remaining_tokens();
+
+ $parser = new WP_MySQL_Parser( $this->grammar, $tokens );
+ $ast = $parser->parse();
+ $expr = $this->translate_query( $ast );
+ //$expr = $this->rewrite_sql_calc_found_rows( $expr );
+
+ if ( null === $expr ) {
+ return false;
+ }
+
+ $sqlite_query = WP_SQLite_Query_Builder::stringify( $expr );
+
+ // Returning the query just for now for testing. In the end, we'll
+ // run it and return the SQLite interaction result.
+ //return $sqlite_query;
+
+ if ( ! $sqlite_query ) {
+ return false;
+ }
+
+ $is_select = (bool) $ast->get_descendant( 'selectStatement' );
+ $statement = $this->pdo->prepare( $sqlite_query );
+ $return_value = $statement->execute();
+ $this->results = $return_value;
+ if ( $is_select ) {
+ $this->results = $statement->fetchAll( PDO::FETCH_OBJ );
+ }
+ return $return_value;
+ }
+
+ public function get_error_message() {
+ }
+
+ public function get_query_results() {
+ return $this->results;
+ }
+
+ private function rewrite_sql_calc_found_rows( WP_SQLite_Expression $expr ) {
+ if ( $this->has_found_rows_call && ! $this->has_sql_calc_found_rows && null === $this->last_calc_rows_result ) {
+ throw new Exception( 'FOUND_ROWS() called without SQL_CALC_FOUND_ROWS' );
+ }
+
+ if ( $this->has_sql_calc_found_rows ) {
+ $expr_to_run = $expr;
+ if ( $this->has_found_rows_call ) {
+ $expr_without_found_rows = new WP_SQLite_Expression( array() );
+ foreach ( $expr->elements as $k => $element ) {
+ if ( WP_SQLite_Token::TYPE_IDENTIFIER === $element->type && 'FOUND_ROWS' === $element->value ) {
+ $expr_without_found_rows->add_token(
+ WP_SQLite_Token_Factory::value( 0 )
+ );
+ } else {
+ $expr_without_found_rows->add_token( $element );
+ }
+ }
+ $expr_to_run = $expr_without_found_rows;
+ }
+
+ // ...remove the LIMIT clause...
+ $query = 'SELECT COUNT(*) as cnt FROM (' . WP_SQLite_Query_Builder::stringify( $expr_to_run ) . ');';
+
+ // ...run $query...
+ // $result = ...
+ // $this->last_calc_rows_result = $result['cnt'];
+ }
+
+ if ( ! $this->has_found_rows_call ) {
+ return $expr;
+ }
+
+ $expr_with_found_rows_result = new WP_SQLite_Expression( array() );
+ foreach ( $expr->elements as $k => $element ) {
+ if ( WP_SQLite_Token::TYPE_IDENTIFIER === $element->type && 'FOUND_ROWS' === $element->value ) {
+ $expr_with_found_rows_result->add_token(
+ WP_SQLite_Token_Factory::value( $this->last_calc_rows_result )
+ );
+ } else {
+ $expr_with_found_rows_result->add_token( $element );
+ }
+ }
+ return $expr_with_found_rows_result;
+ }
+
+ private function translate_query( $ast ) {
+ if ( null === $ast ) {
+ return null;
+ }
+
+ if ( $ast instanceof WP_MySQL_Token ) {
+ $token = $ast;
+ switch ( $token->type ) {
+ case WP_MySQL_Lexer::EOF:
+ return new WP_SQLite_Expression( array() );
+
+ case WP_MySQL_Lexer::IDENTIFIER:
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::identifier(
+ trim( $token->text, '`"' )
+ ),
+ )
+ );
+
+ default:
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( $token->text ),
+ )
+ );
+ }
+ }
+
+ if ( ! ( $ast instanceof WP_Parser_Node ) ) {
+ throw new Exception( 'translate_query only accepts WP_MySQL_Token and WP_Parser_Node instances' );
+ }
+
+ $rule_name = $ast->rule_name;
+
+ switch ( $rule_name ) {
+ case 'indexHintList':
+ // SQLite doesn't support index hints. Let's skip them.
+ return null;
+
+ case 'querySpecOption':
+ $token = $ast->get_token();
+ switch ( $token->type ) {
+ case WP_MySQL_Lexer::ALL_SYMBOL:
+ case WP_MySQL_Lexer::DISTINCT_SYMBOL:
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( $token->text ),
+ )
+ );
+ case WP_MySQL_Lexer::SQL_CALC_FOUND_ROWS_SYMBOL:
+ $this->has_sql_calc_found_rows = true;
+ // Fall through to default.
+ default:
+ // we'll need to run the current SQL query without any
+ // LIMIT clause, and then substitute the FOUND_ROWS()
+ // function with a literal number of rows found.
+ return new WP_SQLite_Expression( array() );
+ }
+ // Otherwise, fall through.
+
+ case 'fromClause':
+ // Skip `FROM DUAL`. We only care about a singular
+ // FROM DUAL statement, as FROM mytable, DUAL is a syntax
+ // error.
+ if (
+ $ast->has_token( WP_MySQL_Lexer::DUAL_SYMBOL ) &&
+ ! $ast->has_child( 'tableReferenceList' )
+ ) {
+ return null;
+ }
+ // Otherwise, fall through.
+
+ case 'selectOption':
+ case 'interval':
+ case 'intervalTimeStamp':
+ case 'bitExpr':
+ case 'boolPri':
+ case 'lockStrengh':
+ case 'orderList':
+ case 'simpleExpr':
+ case 'columnRef':
+ case 'exprIs':
+ case 'exprAnd':
+ case 'primaryExprCompare':
+ case 'fieldIdentifier':
+ case 'dotIdentifier':
+ case 'identifier':
+ case 'literal':
+ case 'joinedTable':
+ case 'nullLiteral':
+ case 'boolLiteral':
+ case 'numLiteral':
+ case 'textLiteral':
+ case 'predicate':
+ case 'predicateExprBetween':
+ case 'primaryExprPredicate':
+ case 'pureIdentifier':
+ case 'unambiguousIdentifier':
+ case 'qualifiedIdentifier':
+ case 'query':
+ case 'queryExpression':
+ case 'queryExpressionBody':
+ case 'queryExpressionParens':
+ case 'queryPrimary':
+ case 'querySpecification':
+ case 'queryTerm':
+ case 'selectAlias':
+ case 'selectItem':
+ case 'selectItemList':
+ case 'selectStatement':
+ case 'simpleExprColumnRef':
+ case 'simpleExprFunction':
+ case 'outerJoinType':
+ case 'simpleExprSubQuery':
+ case 'simpleExprLiteral':
+ case 'compOp':
+ case 'simpleExprList':
+ case 'simpleStatement':
+ case 'subquery':
+ case 'exprList':
+ case 'expr':
+ case 'tableReferenceList':
+ case 'tableReference':
+ case 'tableRef':
+ case 'tableAlias':
+ case 'tableFactor':
+ case 'singleTable':
+ case 'udfExprList':
+ case 'udfExpr':
+ case 'withClause':
+ case 'whereClause':
+ case 'commonTableExpression':
+ case 'derivedTable':
+ case 'columnRefOrLiteral':
+ case 'orderClause':
+ case 'groupByClause':
+ case 'lockingClauseList':
+ case 'lockingClause':
+ case 'havingClause':
+ case 'direction':
+ case 'orderExpression':
+ $child_expressions = array();
+ foreach ( $ast->children as $child ) {
+ $child_expressions[] = $this->translate_query( $child );
+ }
+ return new WP_SQLite_Expression( $child_expressions );
+
+ case 'textStringLiteral':
+ return new WP_SQLite_Expression(
+ array(
+ $ast->has_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT ) ?
+ WP_SQLite_Token_Factory::double_quoted_value( $ast->get_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT )->text ) : false,
+ $ast->has_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ) ?
+ WP_SQLite_Token_Factory::raw( $ast->get_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT )->text ) : false,
+ )
+ );
+
+ case 'functionCall':
+ return $this->translate_function_call( $ast );
+
+ case 'runtimeFunctionCall':
+ return $this->translate_runtime_function_call( $ast );
+
+ default:
+ return null;
+ // var_dump(count($ast->children));
+ // foreach($ast->children as $child) {
+ // var_dump(get_class($child));
+ // echo $child->getText();
+ // echo "\n\n";
+ // }
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw(
+ $rule_name
+ ),
+ )
+ );
+ }
+ }
+
+ private function translate_runtime_function_call( $ast ): WP_SQLite_Expression {
+ $name_token = $ast->children[0];
+
+ switch ( strtoupper( $name_token->text ) ) {
+ case 'ADDDATE':
+ case 'DATE_ADD':
+ $args = $ast->get_children( 'expr' );
+ $interval = $ast->get_child( 'interval' );
+ $timespan = $interval->get_child( 'intervalTimeStamp' )->get_token()->text;
+ return WP_SQLite_Token_Factory::create_function(
+ 'DATETIME',
+ array(
+ $this->translate_query( $args[0] ),
+ new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::value( '+' ),
+ WP_SQLite_Token_Factory::raw( '||' ),
+ $this->translate_query( $args[1] ),
+ WP_SQLite_Token_Factory::raw( '||' ),
+ WP_SQLite_Token_Factory::value( $timespan ),
+ )
+ ),
+ )
+ );
+
+ case 'DATE_SUB':
+ // return new WP_SQLite_Expression([
+ // SQLiteTokenFactory::raw("DATETIME("),
+ // $args[0],
+ // SQLiteTokenFactory::raw(", '-'"),
+ // $args[1],
+ // SQLiteTokenFactory::raw(" days')")
+ // ]);
+
+ case 'VALUES':
+ $column = $ast->get_child()->get_descendant( 'pureIdentifier' );
+ if ( ! $column ) {
+ throw new Exception( 'VALUES() calls without explicit column names are unsupported' );
+ }
+
+ $colname = $column->get_token()->extract_value();
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( 'excluded.' ),
+ WP_SQLite_Token_Factory::identifier( $colname ),
+ )
+ );
+ default:
+ throw new Exception( 'Unsupported function: ' . $name_token->text );
+ }
+ }
+
+ private function translate_function_call( $function_call_tree ): WP_SQLite_Expression {
+ $name = $function_call_tree->get_child( 'pureIdentifier' )->get_token()->text;
+ $args = array();
+ foreach ( $function_call_tree->get_child( 'udfExprList' )->get_children() as $node ) {
+ $args[] = $this->translate_query( $node );
+ }
+ switch ( strtoupper( $name ) ) {
+ case 'ABS':
+ case 'ACOS':
+ case 'ASIN':
+ case 'ATAN':
+ case 'ATAN2':
+ case 'COS':
+ case 'DEGREES':
+ case 'TRIM':
+ case 'EXP':
+ case 'MAX':
+ case 'MIN':
+ case 'FLOOR':
+ case 'RADIANS':
+ case 'ROUND':
+ case 'SIN':
+ case 'SQRT':
+ case 'TAN':
+ case 'TRUNCATE':
+ case 'RANDOM':
+ case 'PI':
+ case 'LTRIM':
+ case 'RTRIM':
+ return WP_SQLite_Token_Factory::create_function( $name, $args );
+
+ case 'CEIL':
+ case 'CEILING':
+ return WP_SQLite_Token_Factory::create_function( 'CEIL', $args );
+
+ case 'COT':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( '1 / ' ),
+ WP_SQLite_Token_Factory::create_function( 'TAN', $args ),
+ )
+ );
+
+ case 'LN':
+ case 'LOG':
+ case 'LOG2':
+ return WP_SQLite_Token_Factory::create_function( 'LOG', $args );
+
+ case 'LOG10':
+ return WP_SQLite_Token_Factory::create_function( 'LOG10', $args );
+
+ // case 'MOD':
+ // return $this->transformBinaryOperation([
+ // 'operator' => '%',
+ // 'left' => $args[0],
+ // 'right' => $args[1]
+ // ]);
+
+ case 'POW':
+ case 'POWER':
+ return WP_SQLite_Token_Factory::create_function( 'POW', $args );
+
+ // String functions
+ case 'ASCII':
+ return WP_SQLite_Token_Factory::create_function( 'UNICODE', $args );
+ case 'CHAR_LENGTH':
+ case 'LENGTH':
+ return WP_SQLite_Token_Factory::create_function( 'LENGTH', $args );
+ case 'CONCAT':
+ $concated = array( WP_SQLite_Token_Factory::raw( '(' ) );
+ foreach ( $args as $k => $arg ) {
+ $concated[] = $arg;
+ if ( $k < count( $args ) - 1 ) {
+ $concated[] = WP_SQLite_Token_Factory::raw( '||' );
+ }
+ }
+ $concated[] = WP_SQLite_Token_Factory::raw( ')' );
+ return new WP_SQLite_Expression( $concated );
+ // case 'CONCAT_WS':
+ // return new WP_SQLite_Expression([
+ // SQLiteTokenFactory::raw("REPLACE("),
+ // implode(" || ", array_slice($args, 1)),
+ // SQLiteTokenFactory::raw(", '', "),
+ // $args[0],
+ // SQLiteTokenFactory::raw(")")
+ // ]);
+ case 'INSTR':
+ return WP_SQLite_Token_Factory::create_function( 'INSTR', $args );
+ case 'LCASE':
+ case 'LOWER':
+ return WP_SQLite_Token_Factory::create_function( 'LOWER', $args );
+ case 'LEFT':
+ return WP_SQLite_Token_Factory::create_function(
+ 'SUBSTR',
+ array(
+ $args[0],
+ '1',
+ $args[1],
+ )
+ );
+ case 'LOCATE':
+ return WP_SQLite_Token_Factory::create_function(
+ 'INSTR',
+ array(
+ $args[1],
+ $args[0],
+ )
+ );
+ case 'REPEAT':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( "REPLACE(CHAR(32), ' ', " ),
+ $args[0],
+ WP_SQLite_Token_Factory::raw( ')' ),
+ )
+ );
+
+ case 'REPLACE':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( 'REPLACE(' ),
+ implode( ', ', $args ),
+ WP_SQLite_Token_Factory::raw( ')' ),
+ )
+ );
+ case 'RIGHT':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( 'SUBSTR(' ),
+ $args[0],
+ WP_SQLite_Token_Factory::raw( ', -(' ),
+ $args[1],
+ WP_SQLite_Token_Factory::raw( '))' ),
+ )
+ );
+ case 'SPACE':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( "REPLACE(CHAR(32), ' ', '')" ),
+ )
+ );
+ case 'SUBSTRING':
+ case 'SUBSTR':
+ return WP_SQLite_Token_Factory::create_function( 'SUBSTR', $args );
+ case 'UCASE':
+ case 'UPPER':
+ return WP_SQLite_Token_Factory::create_function( 'UPPER', $args );
+
+ case 'DATE_FORMAT':
+ $mysql_date_format_to_sqlite_strftime = array(
+ '%a' => '%D',
+ '%b' => '%M',
+ '%c' => '%n',
+ '%D' => '%jS',
+ '%d' => '%d',
+ '%e' => '%j',
+ '%H' => '%H',
+ '%h' => '%h',
+ '%I' => '%h',
+ '%i' => '%M',
+ '%j' => '%z',
+ '%k' => '%G',
+ '%l' => '%g',
+ '%M' => '%F',
+ '%m' => '%m',
+ '%p' => '%A',
+ '%r' => '%h:%i:%s %A',
+ '%S' => '%s',
+ '%s' => '%s',
+ '%T' => '%H:%i:%s',
+ '%U' => '%W',
+ '%u' => '%W',
+ '%V' => '%W',
+ '%v' => '%W',
+ '%W' => '%l',
+ '%w' => '%w',
+ '%X' => '%Y',
+ '%x' => '%o',
+ '%Y' => '%Y',
+ '%y' => '%y',
+ );
+ // @TODO: Implement as user defined function to avoid
+ // rewriting something that may be an expression as a string
+ $format = $args[1]->elements[0]->value;
+ $new_format = strtr( $format, $mysql_date_format_to_sqlite_strftime );
+
+ return WP_SQLite_Token_Factory::create_function(
+ 'STRFTIME',
+ array(
+ new WP_SQLite_Expression( array( WP_SQLite_Token_Factory::raw( $new_format ) ) ),
+ new WP_SQLite_Expression( array( $args[0] ) ),
+ )
+ );
+ case 'DATEDIFF':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::create_function( 'JULIANDAY', array( $args[0] ) ),
+ WP_SQLite_Token_Factory::raw( ' - ' ),
+ WP_SQLite_Token_Factory::create_function( 'JULIANDAY', array( $args[1] ) ),
+ )
+ );
+ case 'DAYNAME':
+ return WP_SQLite_Token_Factory::create_function(
+ 'STRFTIME',
+ array_merge( array( '%w' ), $args )
+ );
+ case 'DAY':
+ case 'DAYOFMONTH':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%d' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
+ )
+ );
+ case 'DAYOFWEEK':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%w' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") + 1 AS INTEGER'" ),
+ )
+ );
+ case 'DAYOFYEAR':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%j' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
+ )
+ );
+ case 'HOUR':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%H' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
+ )
+ );
+ case 'MINUTE':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%M' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
+ )
+ );
+ case 'MONTH':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%m' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
+ )
+ );
+ case 'MONTHNAME':
+ return WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%m' ), $args ) );
+ case 'NOW':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( 'CURRENT_TIMESTAMP()' ),
+ )
+ );
+ case 'SECOND':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%S' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
+ )
+ );
+ case 'TIMESTAMP':
+ return new WP_SQLite_Expression(
+ array_merge(
+ array( WP_SQLite_Token_Factory::raw( 'DATETIME(' ) ),
+ $args,
+ array( WP_SQLite_Token_Factory::raw( ')' ) )
+ )
+ );
+ case 'YEAR':
+ return new WP_SQLite_Expression(
+ array(
+ WP_SQLite_Token_Factory::raw( "CAST('" ),
+ WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%Y' ), $args ) ),
+ WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
+ )
+ );
+ case 'FOUND_ROWS':
+ $this->has_found_rows_call = true;
+ return new WP_SQLite_Expression(
+ array(
+ // Post-processed in handleSqlCalcFoundRows()
+ WP_SQLite_Token_Factory::raw( 'FOUND_ROWS' ),
+ )
+ );
+ default:
+ throw new Exception( 'Unsupported function: ' . $name );
+ }
+ }
+}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index cd7e36f..c621078 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -5,662 +5,778 @@
use Exception;
use PDO;
+use PDOException;
+use PDOStatement;
+use SQLite3;
use WP_MySQL_Lexer;
use WP_MySQL_Parser;
-use WP_MySQL_Token;
use WP_Parser_Grammar;
-use WP_Parser_Node;
-
-$grammar = new WP_Parser_Grammar( require __DIR__ . '/../../wp-includes/mysql/mysql-grammar.php' );
+use WP_SQLite_PDO_User_Defined_Functions;
class WP_SQLite_Driver {
+ const GRAMMAR_PATH = __DIR__ . '/../../wp-includes/mysql/mysql-grammar.php';
+
+ const SQLITE_BUSY = 5;
+ const SQLITE_LOCKED = 6;
+
+ const DATA_TYPES_CACHE_TABLE = '_mysql_data_types_cache';
+
+ const CREATE_DATA_TYPES_CACHE_TABLE = 'CREATE TABLE IF NOT EXISTS _mysql_data_types_cache (
+ `table` TEXT NOT NULL,
+ `column_or_index` TEXT NOT NULL,
+ `mysql_type` TEXT NOT NULL,
+ PRIMARY KEY(`table`, `column_or_index`)
+ );';
+
/**
* @var WP_Parser_Grammar
*/
- private $grammar;
+ private static $grammar;
/**
- * @var PDO
+ * Class variable to reference to the PDO instance.
+ *
+ * @access private
+ *
+ * @var PDO object
*/
private $pdo;
- private $results;
+ /**
+ * The database version.
+ *
+ * This is used here to avoid PHP warnings in the health screen.
+ *
+ * @var string
+ */
+ public $client_info = '';
- private $has_sql_calc_found_rows = false;
- private $has_found_rows_call = false;
- private $last_calc_rows_result = null;
+ /**
+ * Last executed MySQL query.
+ *
+ * @var string
+ */
+ public $mysql_query;
- public function __construct( PDO $pdo ) {
- global $grammar;
- $this->pdo = $pdo;
- $this->grammar = $grammar;
+ /**
+ * A list of executed SQLite queries.
+ *
+ * @var array
+ */
+ public $executed_sqlite_queries = array();
- $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
- $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
- $pdo->setAttribute( PDO::ATTR_TIMEOUT, 5 );
- }
+ /**
+ * The affected table name.
+ *
+ * @var array
+ */
+ private $table_name = array();
- public function query( $query ) {
- $this->has_sql_calc_found_rows = false;
- $this->has_found_rows_call = false;
- $this->last_calc_rows_result = null;
+ /**
+ * The type of the executed query (SELECT, INSERT, etc).
+ *
+ * @var array
+ */
+ private $query_type = array();
- $lexer = new WP_MySQL_Lexer( $query );
- $tokens = $lexer->remaining_tokens();
+ /**
+ * The columns to insert.
+ *
+ * @var array
+ */
+ private $insert_columns = array();
- $parser = new WP_MySQL_Parser( $this->grammar, $tokens );
- $ast = $parser->parse();
- $expr = $this->translate_query( $ast );
- //$expr = $this->rewrite_sql_calc_found_rows( $expr );
+ /**
+ * Class variable to store the result of the query.
+ *
+ * @access private
+ *
+ * @var array reference to the PHP object
+ */
+ private $results = null;
- if ( null === $expr ) {
- return false;
+ /**
+ * Class variable to check if there is an error.
+ *
+ * @var boolean
+ */
+ public $is_error = false;
+
+ /**
+ * Class variable to store the file name and function to cause error.
+ *
+ * @access private
+ *
+ * @var array
+ */
+ private $errors;
+
+ /**
+ * Class variable to store the error messages.
+ *
+ * @access private
+ *
+ * @var array
+ */
+ private $error_messages = array();
+
+ /**
+ * Class variable to store the affected row id.
+ *
+ * @var int integer
+ * @access private
+ */
+ private $last_insert_id;
+
+ /**
+ * Class variable to store the number of rows affected.
+ *
+ * @var int integer
+ */
+ private $affected_rows;
+
+ /**
+ * Variable to emulate MySQL affected row.
+ *
+ * @var integer
+ */
+ private $num_rows;
+
+ /**
+ * Return value from query().
+ *
+ * Each query has its own return value.
+ *
+ * @var mixed
+ */
+ private $return_value;
+
+ /**
+ * Variable to keep track of nested transactions level.
+ *
+ * @var int
+ */
+ private $transaction_level = 0;
+
+ /**
+ * Value returned by the last exec().
+ *
+ * @var mixed
+ */
+ private $last_exec_returned;
+
+ /**
+ * The PDO fetch mode passed to query().
+ *
+ * @var mixed
+ */
+ private $pdo_fetch_mode;
+
+ /**
+ * Associative array with list of system (non-WordPress) tables.
+ *
+ * @var array [tablename => tablename]
+ */
+ private $sqlite_system_tables = array();
+
+ /**
+ * The last error message from SQLite.
+ *
+ * @var string
+ */
+ private $last_sqlite_error;
+
+ /**
+ * Constructor.
+ *
+ * Create PDO object, set user defined functions and initialize other settings.
+ * Don't use parent::__construct() because this class does not only returns
+ * PDO instance but many others jobs.
+ *
+ * @param PDO $pdo The PDO object.
+ */
+ public function __construct( $pdo = null ) {
+ if ( ! $pdo ) {
+ if ( ! is_file( FQDB ) ) {
+ $this->prepare_directory();
+ }
+
+ $locked = false;
+ $status = 0;
+ $err_message = '';
+ do {
+ try {
+ $options = array(
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_STRINGIFY_FETCHES => true,
+ PDO::ATTR_TIMEOUT => 5,
+ );
+
+ $dsn = 'sqlite:' . FQDB;
+ $pdo = new PDO( $dsn, null, null, $options ); // phpcs:ignore WordPress.DB.RestrictedClasses
+ } catch ( PDOException $ex ) {
+ $status = $ex->getCode();
+ if ( self::SQLITE_BUSY === $status || self::SQLITE_LOCKED === $status ) {
+ $locked = true;
+ } else {
+ $err_message = $ex->getMessage();
+ }
+ }
+ } while ( $locked );
+
+ if ( $status > 0 ) {
+ $message = sprintf(
+ '%s
%s
%s
',
+ 'Database initialization error!',
+ "Code: $status",
+ "Error Message: $err_message"
+ );
+ $this->is_error = true;
+ $this->error_messages[] = $message;
+ return;
+ }
}
- $sqlite_query = WP_SQLite_Query_Builder::stringify( $expr );
+ new WP_SQLite_PDO_User_Defined_Functions( $pdo );
- // Returning the query just for now for testing. In the end, we'll
- // run it and return the SQLite interaction result.
- //return $sqlite_query;
+ // MySQL data comes across stringified by default.
+ $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
+ $pdo->query( self::CREATE_DATA_TYPES_CACHE_TABLE );
- if ( ! $sqlite_query ) {
- return false;
+ /*
+ * A list of system tables lets us emulate information_schema
+ * queries without returning extra tables.
+ */
+ $this->sqlite_system_tables ['sqlite_sequence'] = 'sqlite_sequence';
+ $this->sqlite_system_tables [ self::DATA_TYPES_CACHE_TABLE ] = self::DATA_TYPES_CACHE_TABLE;
+
+ $this->pdo = $pdo;
+
+ // Load MySQL grammar.
+ if ( null === self::$grammar ) {
+ self::$grammar = new WP_Parser_Grammar( require self::GRAMMAR_PATH );
}
- $is_select = (bool) $ast->get_descendant( 'selectStatement' );
- $statement = $this->pdo->prepare( $sqlite_query );
- $return_value = $statement->execute();
- $this->results = $return_value;
- if ( $is_select ) {
- $this->results = $statement->fetchAll( PDO::FETCH_OBJ );
+ // Fixes a warning in the site-health screen.
+ $this->client_info = SQLite3::version()['versionString'];
+
+ register_shutdown_function( array( $this, '__destruct' ) );
+
+ // WordPress happens to use no foreign keys.
+ $statement = $this->pdo->query( 'PRAGMA foreign_keys' );
+ // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
+ if ( $statement->fetchColumn( 0 ) == '0' ) {
+ $this->pdo->query( 'PRAGMA foreign_keys = ON' );
+ }
+ $this->pdo->query( 'PRAGMA encoding="UTF-8";' );
+
+ $valid_journal_modes = array( 'DELETE', 'TRUNCATE', 'PERSIST', 'MEMORY', 'WAL', 'OFF' );
+ if ( defined( 'SQLITE_JOURNAL_MODE' ) && in_array( SQLITE_JOURNAL_MODE, $valid_journal_modes, true ) ) {
+ $this->pdo->query( 'PRAGMA journal_mode = ' . SQLITE_JOURNAL_MODE );
}
- return $return_value;
}
- public function get_error_message() {
+ /**
+ * Destructor
+ *
+ * If SQLITE_MEM_DEBUG constant is defined, append information about
+ * memory usage into database/mem_debug.txt.
+ *
+ * This definition is changed since version 1.7.
+ */
+ public function __destruct() {
+ if ( defined( 'SQLITE_MEM_DEBUG' ) && SQLITE_MEM_DEBUG ) {
+ $max = ini_get( 'memory_limit' );
+ if ( is_null( $max ) ) {
+ $message = sprintf(
+ '[%s] Memory_limit is not set in php.ini file.',
+ gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] )
+ );
+ error_log( $message );
+ return;
+ }
+ if ( stripos( $max, 'M' ) !== false ) {
+ $max = (int) $max * MB_IN_BYTES;
+ }
+ $peak = memory_get_peak_usage( true );
+ $used = round( (int) $peak / (int) $max * 100, 2 );
+ if ( $used > 90 ) {
+ $message = sprintf(
+ "[%s] Memory peak usage warning: %s %% used. (max: %sM, now: %sM)\n",
+ gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] ),
+ $used,
+ $max,
+ $peak
+ );
+ error_log( $message );
+ }
+ }
}
- public function get_query_results() {
- return $this->results;
+ /**
+ * Get the PDO object.
+ *
+ * @return PDO
+ */
+ public function get_pdo() {
+ return $this->pdo;
+ }
+
+ /**
+ * Method to return inserted row id.
+ */
+ public function get_insert_id() {
+ return $this->last_insert_id;
+ }
+
+ /**
+ * Method to return the number of rows affected.
+ */
+ public function get_affected_rows() {
+ return $this->affected_rows;
}
- private function rewrite_sql_calc_found_rows( WP_SQLite_Expression $expr ) {
- if ( $this->has_found_rows_call && ! $this->has_sql_calc_found_rows && null === $this->last_calc_rows_result ) {
- throw new Exception( 'FOUND_ROWS() called without SQL_CALC_FOUND_ROWS' );
+ /**
+ * Method to execute query().
+ *
+ * Divide the query types into seven different ones. That is to say:
+ *
+ * 1. SELECT SQL_CALC_FOUND_ROWS
+ * 2. INSERT
+ * 3. CREATE TABLE(INDEX)
+ * 4. ALTER TABLE
+ * 5. SHOW VARIABLES
+ * 6. DROP INDEX
+ * 7. THE OTHERS
+ *
+ * #1 is just a tricky play. See the private function handle_sql_count() in query.class.php.
+ * From #2 through #5 call different functions respectively.
+ * #6 call the ALTER TABLE query.
+ * #7 is a normal process: sequentially call prepare_query() and execute_query().
+ *
+ * #1 process has been changed since version 1.5.1.
+ *
+ * @param string $statement Full SQL statement string.
+ * @param int $mode Not used.
+ * @param array ...$fetch_mode_args Not used.
+ *
+ * @see PDO::query()
+ *
+ * @throws Exception If the query could not run.
+ * @throws PDOException If the translated query could not run.
+ *
+ * @return mixed according to the query type
+ */
+ public function query( $statement, $mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) { // phpcs:ignore WordPress.DB.RestrictedClasses
+ $this->flush();
+ if ( function_exists( 'apply_filters' ) ) {
+ /**
+ * Filters queries before they are translated and run.
+ *
+ * Return a non-null value to cause query() to return early with that result.
+ * Use this filter to intercept queries that don't work correctly in SQLite.
+ *
+ * From within the filter you can do
+ * function filter_sql ($result, $translator, $statement, $mode, $fetch_mode_args) {
+ * if ( intercepting this query ) {
+ * return $translator->execute_sqlite_query( $statement );
+ * }
+ * return $result;
+ * }
+ *
+ * @param null|array $result Default null to continue with the query.
+ * @param object $translator The translator object. You can call $translator->execute_sqlite_query().
+ * @param string $statement The statement passed.
+ * @param int $mode Fetch mode: PDO::FETCH_OBJ, PDO::FETCH_CLASS, etc.
+ * @param array $fetch_mode_args Variable arguments passed to query.
+ *
+ * @returns null|array Null to proceed, or an array containing a resultset.
+ * @since 2.1.0
+ */
+ $pre = apply_filters( 'pre_query_sqlite_db', null, $this, $statement, $mode, $fetch_mode_args );
+ if ( null !== $pre ) {
+ return $pre;
+ }
+ }
+ $this->pdo_fetch_mode = $mode;
+ $this->mysql_query = $statement;
+ if (
+ preg_match( '/^\s*START TRANSACTION/i', $statement )
+ || preg_match( '/^\s*BEGIN/i', $statement )
+ ) {
+ return $this->begin_transaction();
+ }
+ if ( preg_match( '/^\s*COMMIT/i', $statement ) ) {
+ return $this->commit();
+ }
+ if ( preg_match( '/^\s*ROLLBACK/i', $statement ) ) {
+ return $this->rollback();
}
- if ( $this->has_sql_calc_found_rows ) {
- $expr_to_run = $expr;
- if ( $this->has_found_rows_call ) {
- $expr_without_found_rows = new WP_SQLite_Expression( array() );
- foreach ( $expr->elements as $k => $element ) {
- if ( WP_SQLite_Token::TYPE_IDENTIFIER === $element->type && 'FOUND_ROWS' === $element->value ) {
- $expr_without_found_rows->add_token(
- WP_SQLite_Token_Factory::value( 0 )
- );
- } else {
- $expr_without_found_rows->add_token( $element );
+ try {
+ // Parse the MySQL query.
+ $lexer = new WP_MySQL_Lexer( $statement );
+ $tokens = $lexer->remaining_tokens();
+
+ $parser = new WP_MySQL_Parser( self::$grammar, $tokens );
+ $ast = $parser->parse();
+
+ if ( null === $ast ) {
+ throw new Exception( 'Failed to parse the MySQL query.' );
+ }
+
+ // Perform all the queries in a nested transaction.
+ $this->begin_transaction();
+
+ do {
+ $error = null;
+ try {
+ $this->execute_mysql_query( $ast );
+ } catch ( PDOException $error ) {
+ if ( $error->getCode() !== self::SQLITE_BUSY ) {
+ throw $error;
}
}
- $expr_to_run = $expr_without_found_rows;
+ } while ( $error );
+
+ if ( function_exists( 'do_action' ) ) {
+ /**
+ * Notifies that a query has been translated and executed.
+ *
+ * @param string $query The executed SQL query.
+ * @param string $query_type The type of the SQL query (e.g. SELECT, INSERT, UPDATE, DELETE).
+ * @param string $table_name The name of the table affected by the SQL query.
+ * @param array $insert_columns The columns affected by the INSERT query (if applicable).
+ * @param int $last_insert_id The ID of the last inserted row (if applicable).
+ * @param int $affected_rows The number of affected rows (if applicable).
+ *
+ * @since 0.1.0
+ */
+ do_action(
+ 'sqlite_translated_query_executed',
+ $this->mysql_query,
+ $this->query_type,
+ $this->table_name,
+ $this->insert_columns,
+ $this->last_insert_id,
+ $this->affected_rows
+ );
}
- // ...remove the LIMIT clause...
- $query = 'SELECT COUNT(*) as cnt FROM (' . WP_SQLite_Query_Builder::stringify( $expr_to_run ) . ');';
+ // Commit the nested transaction.
+ $this->commit();
- // ...run $query...
- // $result = ...
- // $this->last_calc_rows_result = $result['cnt'];
+ return $this->return_value;
+ } catch ( Exception $err ) {
+ // Rollback the nested transaction.
+ $this->rollback();
+ if ( defined( 'PDO_DEBUG' ) && PDO_DEBUG === true ) {
+ throw $err;
+ }
+ return $this->handle_error( $err );
}
+ }
- if ( ! $this->has_found_rows_call ) {
- return $expr;
- }
+ /**
+ * Method to return the queried result data.
+ *
+ * @return mixed
+ */
+ public function get_query_results() {
+ return $this->results;
+ }
- $expr_with_found_rows_result = new WP_SQLite_Expression( array() );
- foreach ( $expr->elements as $k => $element ) {
- if ( WP_SQLite_Token::TYPE_IDENTIFIER === $element->type && 'FOUND_ROWS' === $element->value ) {
- $expr_with_found_rows_result->add_token(
- WP_SQLite_Token_Factory::value( $this->last_calc_rows_result )
- );
- } else {
- $expr_with_found_rows_result->add_token( $element );
- }
+ /**
+ * Method to return the number of rows from the queried result.
+ */
+ public function get_num_rows() {
+ return $this->num_rows;
+ }
+
+ /**
+ * Method to return the queried results according to the query types.
+ *
+ * @return mixed
+ */
+ public function get_return_value() {
+ return $this->return_value;
+ }
+
+ /**
+ * Executes a MySQL query in SQLite.
+ *
+ * @param string $query The query.
+ *
+ * @throws Exception If the query is not supported.
+ */
+ private function execute_mysql_query( $query ) {
+ //@TODO: Implement the query translation.
+ }
+
+ /**
+ * Executes a query in SQLite.
+ *
+ * @param mixed $sql The query to execute.
+ * @param mixed $params The parameters to bind to the query.
+ * @throws PDOException If the query could not be executed.
+ * @return object {
+ * The result of the query.
+ *
+ * @type PDOStatement $stmt The executed statement
+ * @type * $result The value returned by $stmt.
+ * }
+ */
+ public function execute_sqlite_query( $sql, $params = array() ) {
+ $this->executed_sqlite_queries[] = array(
+ 'sql' => $sql,
+ 'params' => $params,
+ );
+
+ $stmt = $this->pdo->prepare( $sql );
+ if ( false === $stmt || null === $stmt ) {
+ $this->last_exec_returned = null;
+ $info = $this->pdo->errorInfo();
+ $this->last_sqlite_error = $info[0] . ' ' . $info[2];
+ throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] );
}
- return $expr_with_found_rows_result;
+ $returned = $stmt->execute( $params );
+ $this->last_exec_returned = $returned;
+ if ( ! $returned ) {
+ $info = $stmt->errorInfo();
+ $this->last_sqlite_error = $info[0] . ' ' . $info[2];
+ throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] );
+ }
+
+ return $stmt;
}
- private function translate_query( $ast ) {
- if ( null === $ast ) {
- return null;
+ /**
+ * Method to return error messages.
+ *
+ * @throws Exception If error is found.
+ *
+ * @return string
+ */
+ public function get_error_message() {
+ if ( count( $this->error_messages ) === 0 ) {
+ $this->is_error = false;
+ $this->error_messages = array();
+ return '';
}
- if ( $ast instanceof WP_MySQL_Token ) {
- $token = $ast;
- switch ( $token->type ) {
- case WP_MySQL_Lexer::EOF:
- return new WP_SQLite_Expression( array() );
-
- case WP_MySQL_Lexer::IDENTIFIER:
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::identifier(
- trim( $token->text, '`"' )
- ),
- )
- );
+ if ( false === $this->is_error ) {
+ return '';
+ }
- default:
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( $token->text ),
- )
- );
- }
+ $output = '
' . PHP_EOL;
+ $output .= '' . PHP_EOL;
+ $output .= '
MySQL query:
' . PHP_EOL;
+ $output .= '
' . $this->mysql_query . '
' . PHP_EOL;
+ $output .= '
Queries made or created this session were:
' . PHP_EOL;
+ $output .= '
' . PHP_EOL;
+ foreach ( $this->executed_sqlite_queries as $q ) {
+ $message = "Executing: {$q['sql']} | " . ( $q['params'] ? 'parameters: ' . implode( ', ', $q['params'] ) : '(no parameters)' );
+
+ $output .= '- ' . htmlspecialchars( $message ) . '
' . PHP_EOL;
+ }
+ $output .= '
' . PHP_EOL;
+ $output .= '
' . PHP_EOL;
+ foreach ( $this->error_messages as $num => $m ) {
+ $output .= '' . PHP_EOL;
+ $output .= sprintf(
+ 'Error occurred at line %1$d in Function %2$s. Error message was: %3$s.',
+ (int) $this->errors[ $num ]['line'],
+ '' . htmlspecialchars( $this->errors[ $num ]['function'] ) . '
',
+ $m
+ ) . PHP_EOL;
+ $output .= '
' . PHP_EOL;
}
- if ( ! ( $ast instanceof WP_Parser_Node ) ) {
- throw new Exception( 'translate_query only accepts WP_MySQL_Token and WP_Parser_Node instances' );
+ try {
+ throw new Exception();
+ } catch ( Exception $e ) {
+ $output .= 'Backtrace:
' . PHP_EOL;
+ $output .= '' . $e->getTraceAsString() . '
' . PHP_EOL;
}
- $rule_name = $ast->rule_name;
-
- switch ( $rule_name ) {
- case 'indexHintList':
- // SQLite doesn't support index hints. Let's skip them.
- return null;
-
- case 'querySpecOption':
- $token = $ast->get_token();
- switch ( $token->type ) {
- case WP_MySQL_Lexer::ALL_SYMBOL:
- case WP_MySQL_Lexer::DISTINCT_SYMBOL:
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( $token->text ),
- )
- );
- case WP_MySQL_Lexer::SQL_CALC_FOUND_ROWS_SYMBOL:
- $this->has_sql_calc_found_rows = true;
- // Fall through to default.
- default:
- // we'll need to run the current SQL query without any
- // LIMIT clause, and then substitute the FOUND_ROWS()
- // function with a literal number of rows found.
- return new WP_SQLite_Expression( array() );
- }
- // Otherwise, fall through.
-
- case 'fromClause':
- // Skip `FROM DUAL`. We only care about a singular
- // FROM DUAL statement, as FROM mytable, DUAL is a syntax
- // error.
- if (
- $ast->has_token( WP_MySQL_Lexer::DUAL_SYMBOL ) &&
- ! $ast->has_child( 'tableReferenceList' )
- ) {
- return null;
- }
- // Otherwise, fall through.
-
- case 'selectOption':
- case 'interval':
- case 'intervalTimeStamp':
- case 'bitExpr':
- case 'boolPri':
- case 'lockStrengh':
- case 'orderList':
- case 'simpleExpr':
- case 'columnRef':
- case 'exprIs':
- case 'exprAnd':
- case 'primaryExprCompare':
- case 'fieldIdentifier':
- case 'dotIdentifier':
- case 'identifier':
- case 'literal':
- case 'joinedTable':
- case 'nullLiteral':
- case 'boolLiteral':
- case 'numLiteral':
- case 'textLiteral':
- case 'predicate':
- case 'predicateExprBetween':
- case 'primaryExprPredicate':
- case 'pureIdentifier':
- case 'unambiguousIdentifier':
- case 'qualifiedIdentifier':
- case 'query':
- case 'queryExpression':
- case 'queryExpressionBody':
- case 'queryExpressionParens':
- case 'queryPrimary':
- case 'querySpecification':
- case 'queryTerm':
- case 'selectAlias':
- case 'selectItem':
- case 'selectItemList':
- case 'selectStatement':
- case 'simpleExprColumnRef':
- case 'simpleExprFunction':
- case 'outerJoinType':
- case 'simpleExprSubQuery':
- case 'simpleExprLiteral':
- case 'compOp':
- case 'simpleExprList':
- case 'simpleStatement':
- case 'subquery':
- case 'exprList':
- case 'expr':
- case 'tableReferenceList':
- case 'tableReference':
- case 'tableRef':
- case 'tableAlias':
- case 'tableFactor':
- case 'singleTable':
- case 'udfExprList':
- case 'udfExpr':
- case 'withClause':
- case 'whereClause':
- case 'commonTableExpression':
- case 'derivedTable':
- case 'columnRefOrLiteral':
- case 'orderClause':
- case 'groupByClause':
- case 'lockingClauseList':
- case 'lockingClause':
- case 'havingClause':
- case 'direction':
- case 'orderExpression':
- $child_expressions = array();
- foreach ( $ast->children as $child ) {
- $child_expressions[] = $this->translate_query( $child );
- }
- return new WP_SQLite_Expression( $child_expressions );
-
- case 'textStringLiteral':
- return new WP_SQLite_Expression(
- array(
- $ast->has_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT ) ?
- WP_SQLite_Token_Factory::double_quoted_value( $ast->get_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT )->text ) : false,
- $ast->has_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ) ?
- WP_SQLite_Token_Factory::raw( $ast->get_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT )->text ) : false,
- )
- );
+ return $output;
+ }
- case 'functionCall':
- return $this->translate_function_call( $ast );
-
- case 'runtimeFunctionCall':
- return $this->translate_runtime_function_call( $ast );
-
- default:
- return null;
- // var_dump(count($ast->children));
- // foreach($ast->children as $child) {
- // var_dump(get_class($child));
- // echo $child->getText();
- // echo "\n\n";
- // }
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw(
- $rule_name
- ),
- )
- );
+ /**
+ * Begin a new transaction or nested transaction.
+ *
+ * @return boolean
+ */
+ public function begin_transaction() {
+ $success = false;
+ try {
+ if ( 0 === $this->transaction_level ) {
+ $this->execute_sqlite_query( 'BEGIN' );
+ } else {
+ $this->execute_sqlite_query( 'SAVEPOINT LEVEL' . $this->transaction_level );
+ }
+ $success = $this->last_exec_returned;
+ } finally {
+ if ( $success ) {
+ ++$this->transaction_level;
+ if ( function_exists( 'do_action' ) ) {
+ /**
+ * Notifies that a transaction-related query has been translated and executed.
+ *
+ * @param string $command The SQL statement (one of "START TRANSACTION", "COMMIT", "ROLLBACK").
+ * @param bool $success Whether the SQL statement was successful or not.
+ * @param int $nesting_level The nesting level of the transaction.
+ *
+ * @since 0.1.0
+ */
+ do_action( 'sqlite_transaction_query_executed', 'START TRANSACTION', (bool) $this->last_exec_returned, $this->transaction_level - 1 );
+ }
+ }
}
+ return $success;
}
- private function translate_runtime_function_call( $ast ): WP_SQLite_Expression {
- $name_token = $ast->children[0];
-
- switch ( strtoupper( $name_token->text ) ) {
- case 'ADDDATE':
- case 'DATE_ADD':
- $args = $ast->get_children( 'expr' );
- $interval = $ast->get_child( 'interval' );
- $timespan = $interval->get_child( 'intervalTimeStamp' )->get_token()->text;
- return WP_SQLite_Token_Factory::create_function(
- 'DATETIME',
- array(
- $this->translate_query( $args[0] ),
- new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::value( '+' ),
- WP_SQLite_Token_Factory::raw( '||' ),
- $this->translate_query( $args[1] ),
- WP_SQLite_Token_Factory::raw( '||' ),
- WP_SQLite_Token_Factory::value( $timespan ),
- )
- ),
- )
- );
+ /**
+ * Commit the current transaction or nested transaction.
+ *
+ * @return boolean True on success, false on failure.
+ */
+ public function commit() {
+ if ( 0 === $this->transaction_level ) {
+ return false;
+ }
- case 'DATE_SUB':
- // return new WP_SQLite_Expression([
- // SQLiteTokenFactory::raw("DATETIME("),
- // $args[0],
- // SQLiteTokenFactory::raw(", '-'"),
- // $args[1],
- // SQLiteTokenFactory::raw(" days')")
- // ]);
-
- case 'VALUES':
- $column = $ast->get_child()->get_descendant( 'pureIdentifier' );
- if ( ! $column ) {
- throw new Exception( 'VALUES() calls without explicit column names are unsupported' );
- }
+ --$this->transaction_level;
+ if ( 0 === $this->transaction_level ) {
+ $this->execute_sqlite_query( 'COMMIT' );
+ } else {
+ $this->execute_sqlite_query( 'RELEASE SAVEPOINT LEVEL' . $this->transaction_level );
+ }
- $colname = $column->get_token()->extract_value();
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( 'excluded.' ),
- WP_SQLite_Token_Factory::identifier( $colname ),
- )
- );
- default:
- throw new Exception( 'Unsupported function: ' . $name_token->text );
+ if ( function_exists( 'do_action' ) ) {
+ do_action( 'sqlite_transaction_query_executed', 'COMMIT', (bool) $this->last_exec_returned, $this->transaction_level );
}
+ return $this->last_exec_returned;
}
- private function translate_function_call( $function_call_tree ): WP_SQLite_Expression {
- $name = $function_call_tree->get_child( 'pureIdentifier' )->get_token()->text;
- $args = array();
- foreach ( $function_call_tree->get_child( 'udfExprList' )->get_children() as $node ) {
- $args[] = $this->translate_query( $node );
+ /**
+ * Rollback the current transaction or nested transaction.
+ *
+ * @return boolean True on success, false on failure.
+ */
+ public function rollback() {
+ if ( 0 === $this->transaction_level ) {
+ return false;
}
- switch ( strtoupper( $name ) ) {
- case 'ABS':
- case 'ACOS':
- case 'ASIN':
- case 'ATAN':
- case 'ATAN2':
- case 'COS':
- case 'DEGREES':
- case 'TRIM':
- case 'EXP':
- case 'MAX':
- case 'MIN':
- case 'FLOOR':
- case 'RADIANS':
- case 'ROUND':
- case 'SIN':
- case 'SQRT':
- case 'TAN':
- case 'TRUNCATE':
- case 'RANDOM':
- case 'PI':
- case 'LTRIM':
- case 'RTRIM':
- return WP_SQLite_Token_Factory::create_function( $name, $args );
-
- case 'CEIL':
- case 'CEILING':
- return WP_SQLite_Token_Factory::create_function( 'CEIL', $args );
-
- case 'COT':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( '1 / ' ),
- WP_SQLite_Token_Factory::create_function( 'TAN', $args ),
- )
- );
- case 'LN':
- case 'LOG':
- case 'LOG2':
- return WP_SQLite_Token_Factory::create_function( 'LOG', $args );
-
- case 'LOG10':
- return WP_SQLite_Token_Factory::create_function( 'LOG10', $args );
-
- // case 'MOD':
- // return $this->transformBinaryOperation([
- // 'operator' => '%',
- // 'left' => $args[0],
- // 'right' => $args[1]
- // ]);
-
- case 'POW':
- case 'POWER':
- return WP_SQLite_Token_Factory::create_function( 'POW', $args );
-
- // String functions
- case 'ASCII':
- return WP_SQLite_Token_Factory::create_function( 'UNICODE', $args );
- case 'CHAR_LENGTH':
- case 'LENGTH':
- return WP_SQLite_Token_Factory::create_function( 'LENGTH', $args );
- case 'CONCAT':
- $concated = array( WP_SQLite_Token_Factory::raw( '(' ) );
- foreach ( $args as $k => $arg ) {
- $concated[] = $arg;
- if ( $k < count( $args ) - 1 ) {
- $concated[] = WP_SQLite_Token_Factory::raw( '||' );
- }
- }
- $concated[] = WP_SQLite_Token_Factory::raw( ')' );
- return new WP_SQLite_Expression( $concated );
- // case 'CONCAT_WS':
- // return new WP_SQLite_Expression([
- // SQLiteTokenFactory::raw("REPLACE("),
- // implode(" || ", array_slice($args, 1)),
- // SQLiteTokenFactory::raw(", '', "),
- // $args[0],
- // SQLiteTokenFactory::raw(")")
- // ]);
- case 'INSTR':
- return WP_SQLite_Token_Factory::create_function( 'INSTR', $args );
- case 'LCASE':
- case 'LOWER':
- return WP_SQLite_Token_Factory::create_function( 'LOWER', $args );
- case 'LEFT':
- return WP_SQLite_Token_Factory::create_function(
- 'SUBSTR',
- array(
- $args[0],
- '1',
- $args[1],
- )
- );
- case 'LOCATE':
- return WP_SQLite_Token_Factory::create_function(
- 'INSTR',
- array(
- $args[1],
- $args[0],
- )
- );
- case 'REPEAT':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "REPLACE(CHAR(32), ' ', " ),
- $args[0],
- WP_SQLite_Token_Factory::raw( ')' ),
- )
- );
+ --$this->transaction_level;
+ if ( 0 === $this->transaction_level ) {
+ $this->execute_sqlite_query( 'ROLLBACK' );
+ } else {
+ $this->execute_sqlite_query( 'ROLLBACK TO SAVEPOINT LEVEL' . $this->transaction_level );
+ }
+ if ( function_exists( 'do_action' ) ) {
+ do_action( 'sqlite_transaction_query_executed', 'ROLLBACK', (bool) $this->last_exec_returned, $this->transaction_level );
+ }
+ return $this->last_exec_returned;
+ }
- case 'REPLACE':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( 'REPLACE(' ),
- implode( ', ', $args ),
- WP_SQLite_Token_Factory::raw( ')' ),
- )
- );
- case 'RIGHT':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( 'SUBSTR(' ),
- $args[0],
- WP_SQLite_Token_Factory::raw( ', -(' ),
- $args[1],
- WP_SQLite_Token_Factory::raw( '))' ),
- )
- );
- case 'SPACE':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "REPLACE(CHAR(32), ' ', '')" ),
- )
- );
- case 'SUBSTRING':
- case 'SUBSTR':
- return WP_SQLite_Token_Factory::create_function( 'SUBSTR', $args );
- case 'UCASE':
- case 'UPPER':
- return WP_SQLite_Token_Factory::create_function( 'UPPER', $args );
-
- case 'DATE_FORMAT':
- $mysql_date_format_to_sqlite_strftime = array(
- '%a' => '%D',
- '%b' => '%M',
- '%c' => '%n',
- '%D' => '%jS',
- '%d' => '%d',
- '%e' => '%j',
- '%H' => '%H',
- '%h' => '%h',
- '%I' => '%h',
- '%i' => '%M',
- '%j' => '%z',
- '%k' => '%G',
- '%l' => '%g',
- '%M' => '%F',
- '%m' => '%m',
- '%p' => '%A',
- '%r' => '%h:%i:%s %A',
- '%S' => '%s',
- '%s' => '%s',
- '%T' => '%H:%i:%s',
- '%U' => '%W',
- '%u' => '%W',
- '%V' => '%W',
- '%v' => '%W',
- '%W' => '%l',
- '%w' => '%w',
- '%X' => '%Y',
- '%x' => '%o',
- '%Y' => '%Y',
- '%y' => '%y',
- );
- // @TODO: Implement as user defined function to avoid
- // rewriting something that may be an expression as a string
- $format = $args[1]->elements[0]->value;
- $new_format = strtr( $format, $mysql_date_format_to_sqlite_strftime );
-
- return WP_SQLite_Token_Factory::create_function(
- 'STRFTIME',
- array(
- new WP_SQLite_Expression( array( WP_SQLite_Token_Factory::raw( $new_format ) ) ),
- new WP_SQLite_Expression( array( $args[0] ) ),
- )
- );
- case 'DATEDIFF':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::create_function( 'JULIANDAY', array( $args[0] ) ),
- WP_SQLite_Token_Factory::raw( ' - ' ),
- WP_SQLite_Token_Factory::create_function( 'JULIANDAY', array( $args[1] ) ),
- )
- );
- case 'DAYNAME':
- return WP_SQLite_Token_Factory::create_function(
- 'STRFTIME',
- array_merge( array( '%w' ), $args )
- );
- case 'DAY':
- case 'DAYOFMONTH':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%d' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
- )
- );
- case 'DAYOFWEEK':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%w' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") + 1 AS INTEGER'" ),
- )
- );
- case 'DAYOFYEAR':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%j' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
- )
- );
- case 'HOUR':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%H' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
- )
- );
- case 'MINUTE':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%M' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
- )
- );
- case 'MONTH':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%m' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
- )
- );
- case 'MONTHNAME':
- return WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%m' ), $args ) );
- case 'NOW':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( 'CURRENT_TIMESTAMP()' ),
- )
- );
- case 'SECOND':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%S' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
- )
- );
- case 'TIMESTAMP':
- return new WP_SQLite_Expression(
- array_merge(
- array( WP_SQLite_Token_Factory::raw( 'DATETIME(' ) ),
- $args,
- array( WP_SQLite_Token_Factory::raw( ')' ) )
- )
- );
- case 'YEAR':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%Y' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
- )
- );
- case 'FOUND_ROWS':
- $this->has_found_rows_call = true;
- return new WP_SQLite_Expression(
- array(
- // Post-processed in handleSqlCalcFoundRows()
- WP_SQLite_Token_Factory::raw( 'FOUND_ROWS' ),
- )
- );
- default:
- throw new Exception( 'Unsupported function: ' . $name );
+ /**
+ * This method makes database directory and .htaccess file.
+ *
+ * It is executed only once when the installation begins.
+ */
+ private function prepare_directory() {
+ global $wpdb;
+ $u = umask( 0000 );
+ if ( ! is_dir( FQDBDIR ) ) {
+ if ( ! @mkdir( FQDBDIR, 0704, true ) ) {
+ umask( $u );
+ wp_die( 'Unable to create the required directory! Please check your server settings.', 'Error!' );
+ }
+ }
+ if ( ! is_writable( FQDBDIR ) ) {
+ umask( $u );
+ $message = 'Unable to create a file in the directory! Please check your server settings.';
+ wp_die( $message, 'Error!' );
+ }
+ if ( ! is_file( FQDBDIR . '.htaccess' ) ) {
+ $fh = fopen( FQDBDIR . '.htaccess', 'w' );
+ if ( ! $fh ) {
+ umask( $u );
+ echo 'Unable to create a file in the directory! Please check your server settings.';
+
+ return false;
+ }
+ fwrite( $fh, 'DENY FROM ALL' );
+ fclose( $fh );
+ }
+ if ( ! is_file( FQDBDIR . 'index.php' ) ) {
+ $fh = fopen( FQDBDIR . 'index.php', 'w' );
+ if ( ! $fh ) {
+ umask( $u );
+ echo 'Unable to create a file in the directory! Please check your server settings.';
+
+ return false;
+ }
+ fwrite( $fh, '' );
+ fclose( $fh );
}
+ umask( $u );
+
+ return true;
+ }
+
+ /**
+ * Method to clear previous data.
+ */
+ private function flush() {
+ $this->mysql_query = '';
+ $this->results = null;
+ $this->last_exec_returned = null;
+ $this->table_name = null;
+ $this->last_insert_id = null;
+ $this->affected_rows = null;
+ $this->insert_columns = array();
+ $this->num_rows = null;
+ $this->return_value = null;
+ $this->error_messages = array();
+ $this->is_error = false;
+ $this->executed_sqlite_queries = array();
+ }
+
+ /**
+ * Error handler.
+ *
+ * @param Exception $err Exception object.
+ *
+ * @return bool Always false.
+ */
+ private function handle_error( Exception $err ) {
+ $message = $err->getMessage();
+ $this->set_error( __LINE__, __FUNCTION__, $message );
+ $this->return_value = false;
+ return false;
+ }
+
+ /**
+ * Method to format the error messages and put out to the file.
+ *
+ * When $wpdb::suppress_errors is set to true or $wpdb::show_errors is set to false,
+ * the error messages are ignored.
+ *
+ * @param string $line Where the error occurred.
+ * @param string $function_name Indicate the function name where the error occurred.
+ * @param string $message The message.
+ *
+ * @return boolean|void
+ */
+ private function set_error( $line, $function_name, $message ) {
+ $this->errors[] = array(
+ 'line' => $line,
+ 'function' => $function_name,
+ );
+ $this->error_messages[] = $message;
+ $this->is_error = true;
}
}
From 62943d6c49956b5dd8c675fced3f80abb827db6e Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 19 Nov 2024 10:10:20 +0100
Subject: [PATCH 005/124] Add a base generic WP_Parser_Token class, add docs
---
tests/bootstrap.php | 5 +-
tests/mysql/WP_MySQL_Lexer_Tests.php | 22 +++----
tests/tools/dump-ast.php | 5 +-
tests/tools/dump-sqlite-query.php | 1 +
tests/tools/run-lexer-benchmark.php | 1 +
tests/tools/run-parser-benchmark.php | 5 +-
wp-includes/mysql/class-wp-mysql-token.php | 58 +++++++++----------
wp-includes/parser/class-wp-parser-node.php | 8 +--
wp-includes/parser/class-wp-parser-token.php | 36 ++++++++++++
wp-includes/parser/class-wp-parser.php | 6 +-
.../class-wp-sqlite-driver-prototype.php | 2 +-
11 files changed, 95 insertions(+), 54 deletions(-)
create mode 100644 wp-includes/parser/class-wp-parser-token.php
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index b663505..4ba0fc7 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -1,11 +1,12 @@
assertTrue( $lexer->next_token() );
- $this->assertSame( WP_MySQL_Lexer::SELECT_SYMBOL, $lexer->get_token()->get_type() );
+ $this->assertSame( WP_MySQL_Lexer::SELECT_SYMBOL, $lexer->get_token()->id );
// id
$this->assertTrue( $lexer->next_token() );
- $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $lexer->get_token()->get_type() );
+ $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $lexer->get_token()->id );
// FROM
$this->assertTrue( $lexer->next_token() );
- $this->assertSame( WP_MySQL_Lexer::FROM_SYMBOL, $lexer->get_token()->get_type() );
+ $this->assertSame( WP_MySQL_Lexer::FROM_SYMBOL, $lexer->get_token()->id );
// users
$this->assertTrue( $lexer->next_token() );
- $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $lexer->get_token()->get_type() );
+ $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $lexer->get_token()->id );
// EOF
$this->assertTrue( $lexer->next_token() );
- $this->assertSame( WP_MySQL_Lexer::EOF, $lexer->get_token()->get_type() );
+ $this->assertSame( WP_MySQL_Lexer::EOF, $lexer->get_token()->id );
// No more tokens.
$this->assertFalse( $lexer->next_token() );
@@ -40,7 +40,7 @@ public function test_tokenize_invalid_input(): void {
// SELECT
$this->assertTrue( $lexer->next_token() );
- $this->assertSame( WP_MySQL_Lexer::SELECT_SYMBOL, $lexer->get_token()->get_type() );
+ $this->assertSame( WP_MySQL_Lexer::SELECT_SYMBOL, $lexer->get_token()->id );
// Invalid input.
$this->assertFalse( $lexer->next_token() );
@@ -66,7 +66,7 @@ public function test_identifier_utf8_range(): void {
$lexer = new WP_MySQL_Lexer( $value );
$this->assertTrue( $lexer->next_token() );
- $type = $lexer->get_token()->get_type();
+ $type = $lexer->get_token()->id;
$is_valid = preg_match( '/^[\x{0080}-\x{ffff}]$/u', $value );
if ( $is_valid ) {
$this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $type );
@@ -95,7 +95,7 @@ public function test_identifier_utf8_two_byte_sequences(): void {
$is_valid = preg_match( '/^[\x{0080}-\x{ffff}]$/u', $value );
if ( $is_valid ) {
$this->assertTrue( $result );
- $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $token->get_type() );
+ $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $token->id );
} else {
$this->assertFalse( $result );
$this->assertNull( $token );
@@ -125,7 +125,7 @@ public function test_identifier_utf8_three_byte_sequences(): void {
$is_valid = preg_match( '/^[\x{0080}-\x{ffff}]$/u', $value );
if ( $is_valid ) {
$this->assertTrue( $result );
- $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $token->get_type() );
+ $this->assertSame( WP_MySQL_Lexer::IDENTIFIER, $token->id );
} else {
$this->assertFalse( $result );
$this->assertNull( $token );
@@ -141,7 +141,7 @@ public function test_identifier_utf8_three_byte_sequences(): void {
public function test_integer_types( $input, $expected ): void {
$lexer = new WP_MySQL_Lexer( $input );
$this->assertTrue( $lexer->next_token() );
- $this->assertSame( $expected, $lexer->get_token()->get_type() );
+ $this->assertSame( $expected, $lexer->get_token()->id );
}
public function data_integer_types(): array {
@@ -185,7 +185,7 @@ public function test_identifier_or_number( $input, $expected ): void {
$lexer = new WP_MySQL_Lexer( $input );
$actual = array_map(
function ( $token ) {
- return $token->get_type();
+ return $token->id;
},
$lexer->remaining_tokens()
);
diff --git a/tests/tools/dump-ast.php b/tests/tools/dump-ast.php
index 5fa512d..0f2b8ee 100644
--- a/tests/tools/dump-ast.php
+++ b/tests/tools/dump-ast.php
@@ -12,12 +12,13 @@ function ( $severity, $message, $file, $line ) {
}
);
-require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php';
-require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php';
require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser.php';
require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-grammar.php';
require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-node.php';
+require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-token.php';
+require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php';
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php';
+require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php';
$grammar_data = include __DIR__ . '/../../wp-includes/mysql/mysql-grammar.php';
$grammar = new WP_Parser_Grammar( $grammar_data );
diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php
index 8b2ada8..82dd244 100644
--- a/tests/tools/dump-sqlite-query.php
+++ b/tests/tools/dump-sqlite-query.php
@@ -3,6 +3,7 @@
require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser.php';
require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-grammar.php';
require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-node.php';
+require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-token.php';
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php';
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php';
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php';
diff --git a/tests/tools/run-lexer-benchmark.php b/tests/tools/run-lexer-benchmark.php
index 5fd0e2c..a86d0f0 100644
--- a/tests/tools/run-lexer-benchmark.php
+++ b/tests/tools/run-lexer-benchmark.php
@@ -12,6 +12,7 @@ function ( $severity, $message, $file, $line ) {
}
);
+require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-token.php';
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php';
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php';
diff --git a/tests/tools/run-parser-benchmark.php b/tests/tools/run-parser-benchmark.php
index 6d16233..5d5f5e2 100644
--- a/tests/tools/run-parser-benchmark.php
+++ b/tests/tools/run-parser-benchmark.php
@@ -13,11 +13,12 @@ function ( $severity, $message, $file, $line ) {
}
);
-require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php';
-require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php';
require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-grammar.php';
require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-node.php';
+require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-token.php';
require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser.php';
+require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php';
+require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php';
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php';
function getStats( $total, $failures, $exceptions ) {
diff --git a/wp-includes/mysql/class-wp-mysql-token.php b/wp-includes/mysql/class-wp-mysql-token.php
index ba112a7..c812288 100644
--- a/wp-includes/mysql/class-wp-mysql-token.php
+++ b/wp-includes/mysql/class-wp-mysql-token.php
@@ -1,39 +1,39 @@
type = $type;
- $this->text = $text;
- }
-
- public function get_type() {
- return $this->type;
+ public function get_name(): string {
+ $name = WP_MySQL_Lexer::get_token_name( $this->id );
+ if ( null === $name ) {
+ $name = 'UNKNOWN';
+ }
+ return $name;
}
- public function get_text() {
- return $this->text;
- }
-
- public function get_name() {
- return WP_MySQL_Lexer::get_token_name( $this->type );
- }
-
- public function extract_value() {
- return $this->get_text();
- }
-
- public function __toString() {
- return $this->text . '<' . $this->type . ',' . $this->get_name() . '>';
+ /**
+ * 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->value . '<' . $this->id . ',' . $this->get_name() . '>';
}
}
diff --git a/wp-includes/parser/class-wp-parser-node.php b/wp-includes/parser/class-wp-parser-node.php
index b65ebd6..75fd486 100644
--- a/wp-includes/parser/class-wp-parser-node.php
+++ b/wp-includes/parser/class-wp-parser-node.php
@@ -113,9 +113,9 @@ public function has_child( $rule_name ) {
public function has_token( $token_id = null ) {
foreach ( $this->children as $child ) {
- if ( $child instanceof WP_MySQL_Token && (
+ if ( $child instanceof WP_Parser_Token && (
null === $token_id ||
- $child->type === $token_id
+ $child->id === $token_id
) ) {
return true;
}
@@ -125,9 +125,9 @@ public function has_token( $token_id = null ) {
public function get_token( $token_id = null ) {
foreach ( $this->children as $child ) {
- if ( $child instanceof WP_MySQL_Token && (
+ if ( $child instanceof WP_Parser_Token && (
null === $token_id ||
- $child->type === $token_id
+ $child->id === $token_id
) ) {
return $child;
}
diff --git a/wp-includes/parser/class-wp-parser-token.php b/wp-includes/parser/class-wp-parser-token.php
new file mode 100644
index 0000000..1148995
--- /dev/null
+++ b/wp-includes/parser/class-wp-parser-token.php
@@ -0,0 +1,36 @@
+id = $id;
+ $this->value = $value;
+ }
+}
diff --git a/wp-includes/parser/class-wp-parser.php b/wp-includes/parser/class-wp-parser.php
index 088f3a2..c7664f5 100644
--- a/wp-includes/parser/class-wp-parser.php
+++ b/wp-includes/parser/class-wp-parser.php
@@ -37,7 +37,7 @@ private function parse_recursive( $rule_id ) {
return true;
}
- if ( $this->tokens[ $this->position ]->type === $rule_id ) {
+ if ( $this->tokens[ $this->position ]->id === $rule_id ) {
++$this->position;
return $this->tokens[ $this->position - 1 ];
}
@@ -52,7 +52,7 @@ private function parse_recursive( $rule_id ) {
// Bale out from processing the current branch if none of its rules can
// possibly match the current token.
if ( isset( $this->grammar->lookahead_is_match_possible[ $rule_id ] ) ) {
- $token_id = $this->tokens[ $this->position ]->type;
+ $token_id = $this->tokens[ $this->position ]->id;
if (
! isset( $this->grammar->lookahead_is_match_possible[ $rule_id ][ $token_id ] ) &&
! isset( $this->grammar->lookahead_is_match_possible[ $rule_id ][ WP_Parser_Grammar::EMPTY_RULE_ID ] )
@@ -101,7 +101,7 @@ private function parse_recursive( $rule_id ) {
// See: https://github.com/mysql/mysql-workbench/blob/8.0.38/library/parsers/grammars/MySQLParser.g4#L994
// See: https://github.com/antlr/antlr4/issues/488
$la = $this->tokens[ $this->position ] ?? null;
- if ( $la && 'selectStatement' === $rule_name && WP_MySQL_Lexer::INTO_SYMBOL === $la->type ) {
+ if ( $la && 'selectStatement' === $rule_name && WP_MySQL_Lexer::INTO_SYMBOL === $la->id ) {
$branch_matches = false;
}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver-prototype.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver-prototype.php
index 98e732a..9296051 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver-prototype.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver-prototype.php
@@ -141,7 +141,7 @@ private function translate_query( $ast ) {
if ( $ast instanceof WP_MySQL_Token ) {
$token = $ast;
- switch ( $token->type ) {
+ switch ( $token->id ) {
case WP_MySQL_Lexer::EOF:
return new WP_SQLite_Expression( array() );
From 3449b0ba6bdaf12d7f76ccf0e9b62ba54413575e Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 19 Nov 2024 16:00:29 +0100
Subject: [PATCH 006/124] Complete WP_Parser_Node helper methods, add tests
---
tests/parser/WP_Parser_Node_Tests.php | 144 +++++++++++++++++
wp-includes/parser/class-wp-parser-node.php | 161 +++++++++++++++-----
wp-includes/parser/class-wp-parser.php | 2 +-
3 files changed, 266 insertions(+), 41 deletions(-)
create mode 100644 tests/parser/WP_Parser_Node_Tests.php
diff --git a/tests/parser/WP_Parser_Node_Tests.php b/tests/parser/WP_Parser_Node_Tests.php
new file mode 100644
index 0000000..e500f83
--- /dev/null
+++ b/tests/parser/WP_Parser_Node_Tests.php
@@ -0,0 +1,144 @@
+assertFalse( $node->has_child() );
+ $this->assertFalse( $node->has_child_node() );
+ $this->assertFalse( $node->has_child_token() );
+
+ $this->assertNull( $node->get_child() );
+ $this->assertNull( $node->get_child_node() );
+ $this->assertNull( $node->get_child_node( 'root' ) );
+ $this->assertNull( $node->get_child_token() );
+ $this->assertNull( $node->get_child_token( 1 ) );
+
+ $this->assertNull( $node->get_descendant_node() );
+ $this->assertNull( $node->get_descendant_token() );
+
+ $this->assertEmpty( $node->get_children() );
+ $this->assertEmpty( $node->get_child_nodes() );
+ $this->assertEmpty( $node->get_child_nodes( 'root' ) );
+ $this->assertEmpty( $node->get_child_tokens() );
+ $this->assertEmpty( $node->get_child_tokens( 1 ) );
+
+ $this->assertEmpty( $node->get_descendants() );
+ $this->assertEmpty( $node->get_descendant_nodes() );
+ $this->assertEmpty( $node->get_descendant_nodes( 'root' ) );
+ $this->assertEmpty( $node->get_descendant_tokens() );
+ $this->assertEmpty( $node->get_descendant_tokens( 1 ) );
+ }
+
+ public function testNodeTree(): void {
+ // Prepare nodes and tokens.
+ $root = new WP_Parser_Node( 1, 'root' );
+ $n_keyword = new WP_Parser_Node( 2, 'keyword' );
+ $n_expr_a = new WP_Parser_Node( 3, 'expr' );
+ $n_expr_b = new WP_Parser_Node( 3, 'expr' );
+ $n_expr_c = new WP_Parser_Node( 3, 'expr' );
+ $t_select = new WP_Parser_Token( 100, 'SELECT' );
+ $t_comma = new WP_Parser_Token( 200, ',' );
+ $t_plus = new WP_Parser_Token( 300, '+' );
+ $t_one = new WP_Parser_Token( 400, '1' );
+ $t_two_a = new WP_Parser_Token( 400, '2' );
+ $t_two_b = new WP_Parser_Token( 400, '2' );
+ $t_eof = new WP_Parser_Token( 500, '' );
+
+ // Prepare a tree.
+ //
+ // A simplified testing tree for an input like "SELECT 1 + 2, 2".
+ //
+ // root
+ // |- keyword
+ // | |- "SELECT"
+ // |- expr [a]
+ // | |- "1"
+ // | |- "+"
+ // | |- expr [c]
+ // | | |- "2" [b]
+ // |- ","
+ // |- expr [b]
+ // | |- "2" [a]
+ // |- EOF
+ $root->append_child( $n_keyword );
+ $root->append_child( $n_expr_a );
+ $root->append_child( $t_comma );
+ $root->append_child( $n_expr_b );
+ $root->append_child( $t_eof );
+
+ $n_keyword->append_child( $t_select );
+ $n_expr_a->append_child( $t_one );
+ $n_expr_a->append_child( $t_plus );
+ $n_expr_a->append_child( $n_expr_c );
+ $n_expr_b->append_child( $t_two_a );
+ $n_expr_c->append_child( $t_two_b );
+
+ // Test "has" methods.
+ $this->assertTrue( $root->has_child() );
+ $this->assertTrue( $root->has_child_node() );
+ $this->assertTrue( $root->has_child_token() );
+
+ // Test single child methods.
+ $this->assertSame( $n_keyword, $root->get_child() );
+ $this->assertSame( $n_keyword, $root->get_child_node() );
+ $this->assertSame( $n_keyword, $root->get_child_node( 'keyword' ) );
+ $this->assertSame( $n_expr_a, $root->get_child_node( 'expr' ) );
+ $this->assertSame( $t_comma, $root->get_child_token() );
+ $this->assertSame( $t_comma, $root->get_child_token( 200 ) );
+ $this->assertNull( $root->get_child_token( 100 ) );
+
+ // Test multiple children methods.
+ $this->assertSame( array( $n_keyword, $n_expr_a, $t_comma, $n_expr_b, $t_eof ), $root->get_children() );
+ $this->assertSame( array( $n_keyword, $n_expr_a, $n_expr_b ), $root->get_child_nodes() );
+ $this->assertSame( array( $n_expr_a, $n_expr_b ), $root->get_child_nodes( 'expr' ) );
+ $this->assertSame( array(), $root->get_child_nodes( 'root' ) );
+ $this->assertSame( array( $t_comma, $t_eof ), $root->get_child_tokens() );
+ $this->assertSame( array( $t_comma ), $root->get_child_tokens( 200 ) );
+ $this->assertSame( array(), $root->get_child_tokens( 100 ) );
+
+ // Test single descendant methods.
+ // @TODO: Consider breadth-first search vs depth-first search.
+ $this->assertSame( $n_keyword, $root->get_descendant_node() );
+ $this->assertSame( $n_expr_a, $root->get_descendant_node( 'expr' ) );
+ $this->assertSame( null, $root->get_descendant_node( 'root' ) );
+ $this->assertSame( $t_comma, $root->get_descendant_token() );
+ $this->assertSame( $t_one, $root->get_descendant_token( 400 ) );
+ $this->assertSame( null, $root->get_descendant_token( 123 ) );
+
+ // Test multiple descendant methods.
+ // @TODO: Consider breadth-first search vs depth-first search.
+ $this->assertSame(
+ array( $n_keyword, $n_expr_a, $t_comma, $n_expr_b, $t_eof, $t_select, $t_one, $t_plus, $n_expr_c, $t_two_a, $t_two_b ),
+ $root->get_descendants()
+ );
+ $this->assertSame(
+ array( $n_keyword, $n_expr_a, $n_expr_b, $n_expr_c ),
+ $root->get_descendant_nodes()
+ );
+ $this->assertSame(
+ array( $n_expr_a, $n_expr_b, $n_expr_c ),
+ $root->get_descendant_nodes( 'expr' )
+ );
+ $this->assertSame(
+ array(),
+ $root->get_descendant_nodes( 'root' )
+ );
+ $this->assertSame(
+ array( $t_comma, $t_eof, $t_select, $t_one, $t_plus, $t_two_a, $t_two_b ),
+ $root->get_descendant_tokens()
+ );
+ $this->assertSame(
+ array( $t_one, $t_two_a, $t_two_b ),
+ $root->get_descendant_tokens( 400 )
+ );
+ $this->assertSame(
+ array(),
+ $root->get_descendant_tokens( 123 )
+ );
+ }
+}
diff --git a/wp-includes/parser/class-wp-parser-node.php b/wp-includes/parser/class-wp-parser-node.php
index 75fd486..89f8666 100644
--- a/wp-includes/parser/class-wp-parser-node.php
+++ b/wp-includes/parser/class-wp-parser-node.php
@@ -15,7 +15,7 @@ class WP_Parser_Node {
*/
public $rule_id;
public $rule_name;
- public $children = array();
+ private $children = array();
public function __construct( $rule_id, $rule_name ) {
$this->rule_id = $rule_id;
@@ -102,83 +102,164 @@ public function merge_fragment( $node ) {
$this->children = array_merge( $this->children, $node->children );
}
- public function has_child( $rule_name ) {
+ public function has_child(): bool {
+ return count( $this->children ) > 0;
+ }
+
+ public function has_child_node( ?string $rule_name = null ): bool {
foreach ( $this->children as $child ) {
- if ( ( $child instanceof WP_Parser_Node && $child->rule_name === $rule_name ) ) {
+ if (
+ $child instanceof WP_Parser_Node
+ && ( null === $rule_name || $child->rule_name === $rule_name )
+ ) {
return true;
}
}
return false;
}
- public function has_token( $token_id = null ) {
+ public function has_child_token( ?int $token_id = null ): bool {
foreach ( $this->children as $child ) {
- if ( $child instanceof WP_Parser_Token && (
- null === $token_id ||
- $child->id === $token_id
- ) ) {
+ if (
+ $child instanceof WP_Parser_Token
+ && ( null === $token_id || $child->id === $token_id )
+ ) {
return true;
}
}
return false;
}
- public function get_token( $token_id = null ) {
+
+ public function get_child() {
+ return $this->children[0] ?? null;
+ }
+
+ public function get_child_node( ?string $rule_name = null ): ?WP_Parser_Node {
foreach ( $this->children as $child ) {
- if ( $child instanceof WP_Parser_Token && (
- null === $token_id ||
- $child->id === $token_id
- ) ) {
+ if (
+ $child instanceof WP_Parser_Node
+ && ( null === $rule_name || $child->rule_name === $rule_name )
+ ) {
return $child;
}
}
return null;
}
- public function get_child( $rule_name = null ) {
+ public function get_child_token( ?int $token_id = null ): ?WP_Parser_Token {
foreach ( $this->children as $child ) {
- if ( $child instanceof WP_Parser_Node && (
- $child->rule_name === $rule_name ||
- null === $rule_name
- ) ) {
+ if (
+ $child instanceof WP_Parser_Token
+ && ( null === $token_id || $child->id === $token_id )
+ ) {
return $child;
}
}
+ return null;
}
- public function get_descendant( $rule_name ) {
- $parse_trees = array( $this );
- while ( count( $parse_trees ) ) {
- $parse_tree = array_pop( $parse_trees );
- if ( $parse_tree->rule_name === $rule_name ) {
- return $parse_tree;
+ public function get_descendant_node( ?string $rule_name = null ): ?WP_Parser_Node {
+ $nodes = array( $this );
+ while ( count( $nodes ) ) {
+ $node = array_shift( $nodes );
+ $child = $node->get_child_node( $rule_name );
+ if ( $child ) {
+ return $child;
+ }
+ $children = $node->get_child_nodes();
+ if ( count( $children ) > 0 ) {
+ array_push( $nodes, ...$children );
}
- array_push( $parse_trees, ...$parse_tree->get_children() );
}
return null;
}
- public function get_descendants( $rule_name ) {
- $parse_trees = array( $this );
+ public function get_descendant_token( ?int $token_id = null ): ?WP_Parser_Token {
+ $nodes = array( $this );
+ while ( count( $nodes ) ) {
+ $node = array_shift( $nodes );
+ $child = $node->get_child_token( $token_id );
+ if ( $child ) {
+ return $child;
+ }
+ $children = $node->get_child_nodes();
+ if ( count( $children ) > 0 ) {
+ array_push( $nodes, ...$children );
+ }
+ }
+ return null;
+ }
+
+ public function get_children(): array {
+ return $this->children;
+ }
+
+ public function get_child_nodes( ?string $rule_name = null ): array {
+ $nodes = array();
+ foreach ( $this->children as $child ) {
+ if (
+ $child instanceof WP_Parser_Node
+ && ( null === $rule_name || $child->rule_name === $rule_name )
+ ) {
+ $nodes[] = $child;
+ }
+ }
+ return $nodes;
+ }
+
+ public function get_child_tokens( ?int $token_id = null ): array {
+ $tokens = array();
+ foreach ( $this->children as $child ) {
+ if (
+ $child instanceof WP_Parser_Token
+ && ( null === $token_id || $child->id === $token_id )
+ ) {
+ $tokens[] = $child;
+ }
+ }
+ return $tokens;
+ }
+
+ public function get_descendants(): array {
+ $nodes = array( $this );
+ $all_descendants = array();
+ while ( count( $nodes ) ) {
+ $node = array_shift( $nodes );
+ $all_descendants = array_merge( $all_descendants, $node->get_children() );
+ $children = $node->get_child_nodes();
+ if ( count( $children ) > 0 ) {
+ array_push( $nodes, ...$children );
+ }
+ }
+ return $all_descendants;
+ }
+
+ public function get_descendant_nodes( ?string $rule_name = null ): array {
+ $nodes = array( $this );
$all_descendants = array();
- while ( count( $parse_trees ) ) {
- $parse_tree = array_pop( $parse_trees );
- $all_descendants = array_merge( $all_descendants, $parse_tree->get_children( $rule_name ) );
- array_push( $parse_trees, ...$parse_tree->get_children() );
+ while ( count( $nodes ) ) {
+ $node = array_shift( $nodes );
+ $all_descendants = array_merge( $all_descendants, $node->get_child_nodes( $rule_name ) );
+ $children = $node->get_child_nodes();
+ if ( count( $children ) > 0 ) {
+ array_push( $nodes, ...$children );
+ }
}
return $all_descendants;
}
- public function get_children( $rule_name = null ) {
- $matches = array();
- foreach ( $this->children as $child ) {
- if ( $child instanceof WP_Parser_Node && (
- null === $rule_name ||
- $child->rule_name === $rule_name
- ) ) {
- $matches[] = $child;
+ public function get_descendant_tokens( ?int $token_id = null ): array {
+ $nodes = array( $this );
+ $all_descendants = array();
+ while ( count( $nodes ) ) {
+ $node = array_shift( $nodes );
+ $all_descendants = array_merge( $all_descendants, $node->get_child_tokens( $token_id ) );
+ $children = $node->get_child_nodes();
+ if ( count( $children ) > 0 ) {
+ array_push( $nodes, ...$children );
}
}
- return $matches;
+ return $all_descendants;
}
}
diff --git a/wp-includes/parser/class-wp-parser.php b/wp-includes/parser/class-wp-parser.php
index c7664f5..f266cc7 100644
--- a/wp-includes/parser/class-wp-parser.php
+++ b/wp-includes/parser/class-wp-parser.php
@@ -115,7 +115,7 @@ private function parse_recursive( $rule_id ) {
return false;
}
- if ( 0 === count( $node->children ) ) {
+ if ( ! $node->has_child() ) {
return true;
}
From 65afd676570a257fad00b641ac8939e4090001a1 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 19 Nov 2024 16:32:00 +0100
Subject: [PATCH 007/124] Add basic support for SELECT statements, add unit
tests
---
tests/WP_SQLite_Driver_Translation_Tests.php | 104 +++++++++++++
tests/tools/dump-sqlite-query.php | 13 +-
.../sqlite-ast/class-wp-sqlite-driver.php | 138 ++++++++++++++++--
3 files changed, 243 insertions(+), 12 deletions(-)
create mode 100644 tests/WP_SQLite_Driver_Translation_Tests.php
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
new file mode 100644
index 0000000..a9de6e0
--- /dev/null
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -0,0 +1,104 @@
+assertQuery(
+ 'SELECT 1',
+ 'SELECT 1'
+ );
+
+ $this->assertQuery(
+ 'SELECT * FROM "t"',
+ 'SELECT * FROM t'
+ );
+
+ $this->assertQuery(
+ 'SELECT "c" FROM "t"',
+ 'SELECT c FROM t'
+ );
+
+ $this->assertQuery(
+ 'SELECT ALL "c" FROM "t"',
+ 'SELECT ALL c FROM t'
+ );
+
+ $this->assertQuery(
+ 'SELECT DISTINCT "c" FROM "t"',
+ 'SELECT DISTINCT c FROM t'
+ );
+
+ $this->assertQuery(
+ 'SELECT "c1" , "c2" FROM "t"',
+ 'SELECT c1, c2 FROM t'
+ );
+
+ $this->assertQuery(
+ 'SELECT "t"."c" FROM "t"',
+ 'SELECT t.c FROM t'
+ );
+
+ $this->assertQuery(
+ 'SELECT "c1" FROM "t" WHERE "c2" = \'abc\'',
+ "SELECT c1 FROM t WHERE c2 = 'abc'"
+ );
+
+ $this->assertQuery(
+ 'SELECT "c" FROM "t" GROUP BY "c"',
+ 'SELECT c FROM t GROUP BY c'
+ );
+
+ $this->assertQuery(
+ 'SELECT "c" FROM "t" ORDER BY "c" ASC',
+ 'SELECT c FROM t ORDER BY c ASC'
+ );
+
+ $this->assertQuery(
+ 'SELECT "c" FROM "t" LIMIT 10',
+ 'SELECT c FROM t LIMIT 10'
+ );
+
+ $this->assertQuery(
+ 'SELECT "c" FROM "t" GROUP BY "c" HAVING COUNT ( "c" ) > 1',
+ 'SELECT c FROM t GROUP BY c HAVING COUNT(c) > 1'
+ );
+
+ $this->assertQuery(
+ 'SELECT * FROM "t1" LEFT JOIN "t2" ON "t1"."id" = "t2"."t1_id" WHERE "t1"."name" = \'abc\'',
+ "SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id WHERE t1.name = 'abc'"
+ );
+ }
+
+ private function assertQuery( $expected, string $query ): void {
+ $driver = new WP_SQLite_Driver( new PDO( 'sqlite::memory:' ) );
+ $driver->query( $query );
+
+ $executed_queries = array_column( $driver->executed_sqlite_queries, 'sql' );
+ if ( count( $executed_queries ) > 2 ) {
+ // Remove BEGIN and COMMIT/ROLLBACK queries.
+ $executed_queries = array_values( array_slice( $executed_queries, 1, -1, true ) );
+ }
+
+ if ( ! is_array( $expected ) ) {
+ $expected = array( $expected );
+ }
+ $this->assertSame( $expected, $executed_queries );
+ }
+}
diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php
index 82dd244..158d79d 100644
--- a/tests/tools/dump-sqlite-query.php
+++ b/tests/tools/dump-sqlite-query.php
@@ -20,4 +20,15 @@
$query = "SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id WHERE t1.name = 'abc'";
-echo $driver->query( $query );
+$driver->query( $query );
+
+$executed_queries = $driver->executed_sqlite_queries;
+if ( count( $executed_queries ) > 2 ) {
+ // Remove BEGIN and COMMIT/ROLLBACK queries.
+ $executed_queries = array_values( array_slice( $executed_queries, 1, -1, true ) );
+}
+
+foreach ( $executed_queries as $executed_query ) {
+ printf( "Query: %s\n", $executed_query['sql'] );
+ printf( "Params: %s\n", json_encode( $executed_query['params'] ) );
+}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index c621078..f24c992 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -10,7 +10,9 @@
use SQLite3;
use WP_MySQL_Lexer;
use WP_MySQL_Parser;
+use WP_MySQL_Token;
use WP_Parser_Grammar;
+use WP_Parser_Node;
use WP_SQLite_PDO_User_Defined_Functions;
class WP_SQLite_Driver {
@@ -502,17 +504,6 @@ public function get_return_value() {
return $this->return_value;
}
- /**
- * Executes a MySQL query in SQLite.
- *
- * @param string $query The query.
- *
- * @throws Exception If the query is not supported.
- */
- private function execute_mysql_query( $query ) {
- //@TODO: Implement the query translation.
- }
-
/**
* Executes a query in SQLite.
*
@@ -681,6 +672,93 @@ public function rollback() {
return $this->last_exec_returned;
}
+ /**
+ * Executes a MySQL query in SQLite.
+ *
+ * @param string $query The query.
+ *
+ * @throws Exception If the query is not supported.
+ */
+ private function execute_mysql_query( WP_Parser_Node $ast ) {
+ if ( 'query' !== $ast->rule_name ) {
+ throw new Exception( sprintf( 'Expected "query" node, got: "%s"', $ast->rule_name ) );
+ }
+
+ $children = $ast->get_child_nodes();
+ if ( count( $children ) !== 1 ) {
+ throw new Exception( sprintf( 'Expected 1 child, got: %d', count( $children ) ) );
+ }
+
+ $ast = $children[0]->get_child_node();
+ switch ( $ast->rule_name ) {
+ case 'selectStatement':
+ $this->query_type = 'SELECT';
+ $query = $this->translate( $ast->get_child() );
+ $stmt = $this->execute_sqlite_query( $query );
+ $this->set_results_from_fetched_data(
+ $stmt->fetchAll( $this->pdo_fetch_mode )
+ );
+ break;
+ default:
+ throw new Exception( sprintf( 'Unsupported statement type: "%s"', $ast->rule_name ) );
+ }
+ }
+
+ private function translate( $ast ) {
+ if ( null === $ast ) {
+ return null;
+ }
+
+ if ( $ast instanceof WP_MySQL_Token ) {
+ return $this->translate_token( $ast );
+ }
+
+ if ( ! $ast instanceof WP_Parser_Node ) {
+ throw new Exception( 'translate_query only accepts WP_MySQL_Token and WP_Parser_Node instances' );
+ }
+
+ $rule_name = $ast->rule_name;
+ switch ( $rule_name ) {
+ case 'qualifiedIdentifier':
+ case 'dotIdentifier':
+ return $this->translate_sequence( $ast->get_children(), '' );
+ case 'textStringLiteral':
+ if ( $ast->has_child_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT ) ) {
+ return WP_SQLite_Token_Factory::double_quoted_value(
+ $ast->get_child_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT )->value
+ )->value;
+ }
+ if ( $ast->has_child_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ) ) {
+ return WP_SQLite_Token_Factory::raw(
+ $ast->get_child_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT )->value
+ )->value;
+ }
+ // Fall through to the default case.
+
+ default:
+ return $this->translate_sequence( $ast->get_children() );
+ }
+ }
+
+ private function translate_token( WP_MySQL_Token $token ) {
+ switch ( $token->id ) {
+ case WP_MySQL_Lexer::EOF:
+ return null;
+ case WP_MySQL_Lexer::IDENTIFIER:
+ return '"' . trim( $token->value, '`"' ) . '"';
+ default:
+ return $token->value;
+ }
+ }
+
+ private function translate_sequence( array $nodes, string $separator = ' ' ): string {
+ $parts = array();
+ foreach ( $nodes as $node ) {
+ $parts[] = $this->translate( $node );
+ }
+ return implode( $separator, $parts );
+ }
+
/**
* This method makes database directory and .htaccess file.
*
@@ -745,6 +823,44 @@ private function flush() {
$this->executed_sqlite_queries = array();
}
+ /**
+ * Method to set the results from the fetched data.
+ *
+ * @param array $data The data to set.
+ */
+ private function set_results_from_fetched_data( $data ) {
+ if ( null === $this->results ) {
+ $this->results = $data;
+ }
+ if ( is_array( $this->results ) ) {
+ $this->num_rows = count( $this->results );
+ $this->last_select_found_rows = count( $this->results );
+ }
+ $this->return_value = $this->results;
+ }
+
+ /**
+ * Method to set the results from the affected rows.
+ *
+ * @param int|null $override Override the affected rows.
+ */
+ private function set_result_from_affected_rows( $override = null ) {
+ /*
+ * SELECT CHANGES() is a workaround for the fact that
+ * $stmt->rowCount() returns "0" (zero) with the
+ * SQLite driver at all times.
+ * Source: https://www.php.net/manual/en/pdostatement.rowcount.php
+ */
+ if ( null === $override ) {
+ $this->affected_rows = (int) $this->execute_sqlite_query( 'select changes()' )->fetch()[0];
+ } else {
+ $this->affected_rows = $override;
+ }
+ $this->return_value = $this->affected_rows;
+ $this->num_rows = $this->affected_rows;
+ $this->results = $this->affected_rows;
+ }
+
/**
* Error handler.
*
From 00ec46d2fb4a8a5fface35e6c34b44a0235fcb77 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 20 Nov 2024 16:15:49 +0100
Subject: [PATCH 008/124] Add basic support for INSERT, UPDATE, REPLACE, DELETE
---
tests/WP_SQLite_Driver_Translation_Tests.php | 93 +++++++++++++++++++
.../sqlite-ast/class-wp-sqlite-driver.php | 17 ++++
2 files changed, 110 insertions(+)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index a9de6e0..0b75424 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -86,6 +86,99 @@ public function testSelect(): void {
);
}
+ public function testInsert(): void {
+ $this->assertQuery(
+ 'INSERT INTO "t" ( "c" ) VALUES ( 1 )',
+ 'INSERT INTO t (c) VALUES (1)'
+ );
+
+ $this->assertQuery(
+ 'INSERT INTO "s"."t" ( "c" ) VALUES ( 1 )',
+ 'INSERT INTO s.t (c) VALUES (1)'
+ );
+
+ $this->assertQuery(
+ 'INSERT INTO "t" ( "c1" , "c2" ) VALUES ( 1 , 2 )',
+ 'INSERT INTO t (c1, c2) VALUES (1, 2)'
+ );
+
+ $this->assertQuery(
+ 'INSERT INTO "t" ( "c" ) VALUES ( 1 ) , ( 2 )',
+ 'INSERT INTO t (c) VALUES (1), (2)'
+ );
+
+ $this->assertQuery(
+ 'INSERT INTO "t1" SELECT * FROM "t2"',
+ 'INSERT INTO t1 SELECT * FROM t2'
+ );
+ }
+
+ public function testReplace(): void {
+ $this->assertQuery(
+ 'REPLACE INTO "t" ( "c" ) VALUES ( 1 )',
+ 'REPLACE INTO t (c) VALUES (1)'
+ );
+
+ $this->assertQuery(
+ 'REPLACE INTO "s"."t" ( "c" ) VALUES ( 1 )',
+ 'REPLACE INTO s.t (c) VALUES (1)'
+ );
+
+ $this->assertQuery(
+ 'REPLACE INTO "t" ( "c1" , "c2" ) VALUES ( 1 , 2 )',
+ 'REPLACE INTO t (c1, c2) VALUES (1, 2)'
+ );
+
+ $this->assertQuery(
+ 'REPLACE INTO "t" ( "c" ) VALUES ( 1 ) , ( 2 )',
+ 'REPLACE INTO t (c) VALUES (1), (2)'
+ );
+
+ $this->assertQuery(
+ 'REPLACE INTO "t1" SELECT * FROM "t2"',
+ 'REPLACE INTO t1 SELECT * FROM t2'
+ );
+ }
+
+ public function testUpdate(): void {
+ $this->assertQuery(
+ 'UPDATE "t" SET "c" = 1',
+ 'UPDATE t SET c = 1'
+ );
+
+ $this->assertQuery(
+ 'UPDATE "s"."t" SET "c" = 1',
+ 'UPDATE s.t SET c = 1'
+ );
+
+ $this->assertQuery(
+ 'UPDATE "t" SET "c1" = 1 , "c2" = 2',
+ 'UPDATE t SET c1 = 1, c2 = 2'
+ );
+
+ $this->assertQuery(
+ 'UPDATE "t" SET "c" = 1 WHERE "c" = 2',
+ 'UPDATE t SET c = 1 WHERE c = 2'
+ );
+ }
+
+ public function testDelete(): void {
+ $this->assertQuery(
+ 'DELETE FROM "t"',
+ 'DELETE FROM t'
+ );
+
+ $this->assertQuery(
+ 'DELETE FROM "s"."t"',
+ 'DELETE FROM s.t'
+ );
+
+ $this->assertQuery(
+ 'DELETE FROM "t" WHERE "c" = 1',
+ 'DELETE FROM t WHERE c = 1'
+ );
+ }
+
private function assertQuery( $expected, string $query ): void {
$driver = new WP_SQLite_Driver( new PDO( 'sqlite::memory:' ) );
$driver->query( $query );
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index f24c992..328fc77 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -699,6 +699,23 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$stmt->fetchAll( $this->pdo_fetch_mode )
);
break;
+ case 'insertStatement':
+ case 'updateStatement':
+ case 'replaceStatement':
+ case 'deleteStatement':
+ if ( 'insertStatement' === $ast->rule_name ) {
+ $this->query_type = 'INSERT';
+ } elseif ( 'updateStatement' === $ast->rule_name ) {
+ $this->query_type = 'UPDATE';
+ } elseif ( 'replaceStatement' === $ast->rule_name ) {
+ $this->query_type = 'REPLACE';
+ } elseif ( 'deleteStatement' === $ast->rule_name ) {
+ $this->query_type = 'DELETE';
+ }
+ $query = $this->translate( $ast );
+ $this->execute_sqlite_query( $query );
+ $this->set_result_from_affected_rows();
+ break;
default:
throw new Exception( sprintf( 'Unsupported statement type: "%s"', $ast->rule_name ) );
}
From b8b45009f467507f291dc9b98ec705361207355a Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 20 Nov 2024 17:00:36 +0100
Subject: [PATCH 009/124] Add basic support for CREATE TABLE, implement data
types
---
tests/WP_SQLite_Driver_Translation_Tests.php | 133 ++++++++++++++++-
.../WP_MySQL_Server_Suite_Parser_Tests.php | 1 -
wp-includes/mysql/class-wp-mysql-lexer.php | 4 +
.../sqlite-ast/class-wp-sqlite-driver.php | 140 ++++++++++++++++--
4 files changed, 267 insertions(+), 11 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 0b75424..075da64 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -179,16 +179,147 @@ public function testDelete(): void {
);
}
+ public function testCreateTable(): void {
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "id" INTEGER )',
+ 'CREATE TABLE t (id INT)'
+ );
+
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "id" INTEGER , "name" TEXT , "score" REAL DEFAULT 0.0 )',
+ 'CREATE TABLE t (id INT, name TEXT, score FLOAT DEFAULT 0.0)'
+ );
+
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT )',
+ 'CREATE TABLE t (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT)'
+ );
+
+ $this->assertQuery(
+ 'CREATE TABLE IF NOT EXISTS "t" ( "id" INTEGER )',
+ 'CREATE TABLE IF NOT EXISTS t (id INT)'
+ );
+ }
+
+ public function testDataTypes(): void {
+ // Numeric data types.
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "i1" INTEGER , "i2" INTEGER , "i3" INTEGER )',
+ 'CREATE TABLE t (i1 BIT, i2 BOOL, i3 BOOLEAN)'
+ );
+
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "i1" INTEGER , "i2" INTEGER , "i3" INTEGER , "i4" INTEGER , "i5" INTEGER , "i6" INTEGER )',
+ 'CREATE TABLE t (i1 TINYINT, i2 SMALLINT, i3 MEDIUMINT, i4 INT, i5 INTEGER, i6 BIGINT)'
+ );
+
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "f1" REAL , "f2" REAL , "f3" REAL , "f4" REAL )',
+ 'CREATE TABLE t (f1 FLOAT, f2 DOUBLE, f3 DOUBLE PRECISION, f4 REAL)'
+ );
+
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "f1" REAL , "f2" REAL , "f3" REAL , "f4" REAL )',
+ 'CREATE TABLE t (f1 DECIMAL, f2 DEC, f3 FIXED, f4 NUMERIC)'
+ );
+
+ // String data types.
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "c1" TEXT , "c2" TEXT , "c3" TEXT , "c4" TEXT )',
+ 'CREATE TABLE t (c1 CHAR, c2 VARCHAR(255), c3 CHAR VARYING(255), c4 CHARACTER VARYING(255))'
+ );
+
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "c1" TEXT , "c2" TEXT )',
+ 'CREATE TABLE t (c1 NATIONAL CHAR, c2 NCHAR)'
+ );
+
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "c1" TEXT , "c2" TEXT , "c3" TEXT )',
+ 'CREATE TABLE t (c1 NCHAR VARCHAR(255), c2 NCHAR VARYING(255), c3 NVARCHAR(255))'
+ );
+
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "c1" TEXT , "c2" TEXT , "c3" TEXT )',
+ 'CREATE TABLE t (c1 NATIONAL VARCHAR(255), c2 NATIONAL CHAR VARYING(255), c3 NATIONAL CHARACTER VARYING(255))'
+ );
+
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "t1" TEXT , "t2" TEXT , "t3" TEXT , "t4" TEXT )',
+ 'CREATE TABLE t (t1 TINYTEXT, t2 TEXT, t3 MEDIUMTEXT, t4 LONGTEXT)'
+ );
+
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "e" TEXT )',
+ 'CREATE TABLE t (e ENUM("a", "b", "c"))'
+ );
+
+ // Date and time data types.
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "d" TEXT , "t" TEXT , "dt" TEXT , "ts" TEXT , "y" TEXT )',
+ 'CREATE TABLE t (d DATE, t TIME, dt DATETIME, ts TIMESTAMP, y YEAR)'
+ );
+
+ // Binary data types.
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "b" INTEGER , "v" BLOB )',
+ 'CREATE TABLE t (b BINARY, v VARBINARY(255))'
+ );
+
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "b1" BLOB , "b2" BLOB , "b3" BLOB , "b4" BLOB )',
+ 'CREATE TABLE t (b1 TINYBLOB, b2 BLOB, b3 MEDIUMBLOB, b4 LONGBLOB)'
+ );
+
+ // Spatial data types.
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "g1" TEXT , "g2" TEXT , "g3" TEXT , "g4" TEXT )',
+ 'CREATE TABLE t (g1 GEOMETRY, g2 POINT, g3 LINESTRING, g4 POLYGON)'
+ );
+
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "g1" TEXT , "g2" TEXT , "g3" TEXT )',
+ 'CREATE TABLE t (g1 MULTIPOINT, g2 MULTILINESTRING, g3 MULTIPOLYGON)'
+ );
+
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "g1" TEXT , "g2" TEXT )',
+ 'CREATE TABLE t (g1 GEOMCOLLECTION, g2 GEOMETRYCOLLECTION)'
+ );
+
+ // SERIAL
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE )',
+ 'CREATE TABLE t (id SERIAL)'
+ );
+ }
+
private function assertQuery( $expected, string $query ): void {
$driver = new WP_SQLite_Driver( new PDO( 'sqlite::memory:' ) );
$driver->query( $query );
+ // Check for SQLite syntax errors.
+ // This ensures that invalid SQLite syntax will always fail, even if it
+ // was the expected result. It prevents us from using wrong assertions.
+ $error = $driver->get_error_message();
+ if ( $error && preg_match( '/(SQLSTATE\[HY000].+syntax error\.)/i', $error, $matches ) ) {
+ $this->fail( 'SQLite syntax error: ' . $matches[1] );
+ }
+
$executed_queries = array_column( $driver->executed_sqlite_queries, 'sql' );
+
+ // Remove BEGIN and COMMIT/ROLLBACK queries.
if ( count( $executed_queries ) > 2 ) {
- // Remove BEGIN and COMMIT/ROLLBACK queries.
$executed_queries = array_values( array_slice( $executed_queries, 1, -1, true ) );
}
+ // Remove "select changes()" executed after some queries.
+ if (
+ count( $executed_queries ) > 1
+ && 'select changes()' === $executed_queries[ count( $executed_queries ) - 1 ] ) {
+ array_pop( $executed_queries );
+ }
+
if ( ! is_array( $expected ) ) {
$expected = array( $expected );
}
diff --git a/tests/mysql/WP_MySQL_Server_Suite_Parser_Tests.php b/tests/mysql/WP_MySQL_Server_Suite_Parser_Tests.php
index 6a8a96d..e6bb128 100644
--- a/tests/mysql/WP_MySQL_Server_Suite_Parser_Tests.php
+++ b/tests/mysql/WP_MySQL_Server_Suite_Parser_Tests.php
@@ -17,7 +17,6 @@ class WP_MySQL_Server_Suite_Parser_Tests extends TestCase {
'SELECT 1 /*!99999 /* */ */' => true,
'select 1ea10.1a20,1e+ 1e+10 from 1ea10' => true,
"聠聡聢聣聤聬聭聮聯聰聲聽隆垄拢陇楼卤潞禄录陆戮 聶職聳聴\n0聲5\n1聲5\n2聲5\n3聲5\n4\n\nSET NAMES gb18030" => true,
- 'CREATE TABLE t1 (g GEOMCOLLECTION)' => true,
"alter user mysqltest_7@ identified by 'systpass'" => true,
"SELECT 'a%' LIKE 'a!%' ESCAPE '!', 'a%' LIKE 'a!' || '%' ESCAPE '!'" => true,
"SELECT 'a%' NOT LIKE 'a!%' ESCAPE '!', 'a%' NOT LIKE 'a!' || '%' ESCAPE '!'" => true,
diff --git a/wp-includes/mysql/class-wp-mysql-lexer.php b/wp-includes/mysql/class-wp-mysql-lexer.php
index bcaca57..f2174ea 100644
--- a/wp-includes/mysql/class-wp-mysql-lexer.php
+++ b/wp-includes/mysql/class-wp-mysql-lexer.php
@@ -929,6 +929,7 @@ class WP_MySQL_Lexer {
const SECONDARY_ENGINE_ATTRIBUTE_SYMBOL = 849;
const JSON_VALUE_SYMBOL = 850;
const RETURNING_SYMBOL = 851;
+ const GEOMCOLLECTION_SYMBOL = 852;
// Comments
const COMMENT = 900;
@@ -1155,6 +1156,7 @@ class WP_MySQL_Lexer {
'FUNCTION' => self::FUNCTION_SYMBOL,
'GENERAL' => self::GENERAL_SYMBOL,
'GENERATED' => self::GENERATED_SYMBOL,
+ 'GEOMCOLLECTION' => self::GEOMCOLLECTION_SYMBOL,
'GEOMETRY' => self::GEOMETRY_SYMBOL,
'GEOMETRYCOLLECTION' => self::GEOMETRYCOLLECTION_SYMBOL,
'GET' => self::GET_SYMBOL,
@@ -1810,6 +1812,7 @@ class WP_MySQL_Lexer {
self::FIELDS_SYMBOL => self::COLUMNS_SYMBOL,
self::FLOAT4_SYMBOL => self::FLOAT_SYMBOL,
self::FLOAT8_SYMBOL => self::DOUBLE_SYMBOL,
+ self::GEOMCOLLECTION_SYMBOL => self::GEOMETRYCOLLECTION_SYMBOL,
self::INT1_SYMBOL => self::TINYINT_SYMBOL,
self::INT2_SYMBOL => self::SMALLINT_SYMBOL,
self::INT3_SYMBOL => self::MEDIUMINT_SYMBOL,
@@ -1936,6 +1939,7 @@ class WP_MySQL_Lexer {
self::FAILED_LOGIN_ATTEMPTS_SYMBOL => 80019,
self::FIRST_VALUE_SYMBOL => 80000,
self::FOLLOWING_SYMBOL => 80000,
+ self::GEOMCOLLECTION_SYMBOL => 80000,
self::GET_MASTER_PUBLIC_KEY_SYMBOL => 80000,
self::GET_SOURCE_PUBLIC_KEY_SYMBOL => 80000,
self::GROUPING_SYMBOL => 80000,
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 328fc77..d1d4c21 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -21,6 +21,65 @@ class WP_SQLite_Driver {
const SQLITE_BUSY = 5;
const SQLITE_LOCKED = 6;
+ const DATA_TYPE_MAP = array(
+ // Numeric data types:
+ WP_MySQL_Lexer::BIT_SYMBOL => 'INTEGER',
+ WP_MySQL_Lexer::BOOL_SYMBOL => 'INTEGER',
+ WP_MySQL_Lexer::BOOLEAN_SYMBOL => 'INTEGER',
+ WP_MySQL_Lexer::TINYINT_SYMBOL => 'INTEGER',
+ WP_MySQL_Lexer::SMALLINT_SYMBOL => 'INTEGER',
+ WP_MySQL_Lexer::MEDIUMINT_SYMBOL => 'INTEGER',
+ WP_MySQL_Lexer::INT_SYMBOL => 'INTEGER',
+ WP_MySQL_Lexer::INTEGER_SYMBOL => 'INTEGER',
+ WP_MySQL_Lexer::BIGINT_SYMBOL => 'INTEGER',
+ WP_MySQL_Lexer::FLOAT_SYMBOL => 'REAL',
+ WP_MySQL_Lexer::DOUBLE_SYMBOL => 'REAL',
+ WP_MySQL_Lexer::REAL_SYMBOL => 'REAL',
+ WP_MySQL_Lexer::DECIMAL_SYMBOL => 'REAL',
+ WP_MySQL_Lexer::DEC_SYMBOL => 'REAL',
+ WP_MySQL_Lexer::FIXED_SYMBOL => 'REAL',
+ WP_MySQL_Lexer::NUMERIC_SYMBOL => 'REAL',
+
+ // String data types:
+ WP_MySQL_Lexer::CHAR_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::VARCHAR_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::NCHAR_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::NVARCHAR_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::TINYTEXT_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::TEXT_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::MEDIUMTEXT_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::LONGTEXT_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::ENUM_SYMBOL => 'TEXT',
+
+ // Date and time data types:
+ WP_MySQL_Lexer::DATE_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::TIME_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::DATETIME_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::TIMESTAMP_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::YEAR_SYMBOL => 'TEXT',
+
+ // Binary data types:
+ WP_MySQL_Lexer::BINARY_SYMBOL => 'INTEGER',
+ WP_MySQL_Lexer::VARBINARY_SYMBOL => 'BLOB',
+ WP_MySQL_Lexer::TINYBLOB_SYMBOL => 'BLOB',
+ WP_MySQL_Lexer::BLOB_SYMBOL => 'BLOB',
+ WP_MySQL_Lexer::MEDIUMBLOB_SYMBOL => 'BLOB',
+ WP_MySQL_Lexer::LONGBLOB_SYMBOL => 'BLOB',
+
+ // Spatial data types:
+ WP_MySQL_Lexer::GEOMETRY_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::POINT_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::LINESTRING_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::POLYGON_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::MULTIPOINT_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::MULTILINESTRING_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::MULTIPOLYGON_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::GEOMCOLLECTION_SYMBOL => 'TEXT',
+ WP_MySQL_Lexer::GEOMETRYCOLLECTION_SYMBOL => 'TEXT',
+
+ // SERIAL, SET, and JSON types are handled in the translation process.
+ );
+
const DATA_TYPES_CACHE_TABLE = '_mysql_data_types_cache';
const CREATE_DATA_TYPES_CACHE_TABLE = 'CREATE TABLE IF NOT EXISTS _mysql_data_types_cache (
@@ -716,6 +775,25 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$this->execute_sqlite_query( $query );
$this->set_result_from_affected_rows();
break;
+ case 'createStatement':
+ $this->query_type = 'CREATE';
+ $subtree = $ast->get_child_node();
+ switch ( $subtree->rule_name ) {
+ case 'createTable':
+ $query = $this->translate( $ast );
+ $this->execute_sqlite_query( $query );
+ $this->set_result_from_affected_rows();
+ break;
+ default:
+ throw new Exception(
+ sprintf(
+ 'Unsupported statement type: "%s" > "%s"',
+ $ast->rule_name,
+ $subtree->rule_name
+ )
+ );
+ }
+ break;
default:
throw new Exception( sprintf( 'Unsupported statement type: "%s"', $ast->rule_name ) );
}
@@ -739,19 +817,51 @@ private function translate( $ast ) {
case 'qualifiedIdentifier':
case 'dotIdentifier':
return $this->translate_sequence( $ast->get_children(), '' );
+ case 'identifierKeyword':
+ return '"' . $this->translate( $ast->get_child() ) . '"';
case 'textStringLiteral':
- if ( $ast->has_child_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT ) ) {
- return WP_SQLite_Token_Factory::double_quoted_value(
- $ast->get_child_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT )->value
- )->value;
+ $token = $ast->get_child_token();
+ if ( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT === $token->id ) {
+ return WP_SQLite_Token_Factory::double_quoted_value( $token->value )->value;
+ }
+ if ( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT === $token->id ) {
+ return WP_SQLite_Token_Factory::raw( $token->value )->value;
+ }
+ throw $this->invalid_input_exception();
+ case 'dataType':
+ case 'nchar':
+ $child = $ast->get_child();
+ if ( $child instanceof WP_Parser_Node ) {
+ return $this->translate( $child );
+ }
+
+ // Handle optional prefixes (data type is the second token):
+ // 1. LONG VARCHAR, LONG CHAR(ACTER) VARYING, LONG VARBINARY.
+ // 2. NATIONAL CHAR, NATIONAL VARCHAR, NATIONAL CHAR(ACTER) VARYING.
+ if ( WP_MySQL_Lexer::LONG_SYMBOL === $child->id ) {
+ $child = $ast->get_child_tokens()[1] ?? null;
+ } elseif ( WP_MySQL_Lexer::NATIONAL_SYMBOL === $child->id ) {
+ $child = $ast->get_child_tokens()[1] ?? null;
+ }
+
+ if ( null === $child ) {
+ throw $this->invalid_input_exception();
}
- if ( $ast->has_child_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ) ) {
- return WP_SQLite_Token_Factory::raw(
- $ast->get_child_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT )->value
- )->value;
+
+ $type = self::DATA_TYPE_MAP[ $child->id ] ?? null;
+ if ( null !== $type ) {
+ return $type;
+ }
+
+ // SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
+ if ( WP_MySQL_Lexer::SERIAL_SYMBOL === $child->id ) {
+ return 'INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE';
}
- // Fall through to the default case.
+ // @TODO: Handle SET and JSON.
+ throw $this->not_supported_exception(
+ sprintf( 'data type: %s', $child->value )
+ );
default:
return $this->translate_sequence( $ast->get_children() );
}
@@ -763,6 +873,8 @@ private function translate_token( WP_MySQL_Token $token ) {
return null;
case WP_MySQL_Lexer::IDENTIFIER:
return '"' . trim( $token->value, '`"' ) . '"';
+ case WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL:
+ return 'AUTOINCREMENT';
default:
return $token->value;
}
@@ -912,4 +1024,14 @@ private function set_error( $line, $function_name, $message ) {
$this->error_messages[] = $message;
$this->is_error = true;
}
+
+ private function invalid_input_exception() {
+ throw new Exception( 'MySQL query syntax error.' );
+ }
+
+ private function not_supported_exception( string $cause ): Exception {
+ return new Exception(
+ sprintf( 'MySQL query not supported. Cause: %s', $cause )
+ );
+ }
}
From 6be220d13322b100913b8521edc54bd66c74c70c Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 21 Nov 2024 17:57:04 +0100
Subject: [PATCH 010/124] Handle system variables
---
tests/WP_SQLite_Driver_Translation_Tests.php | 17 +++++++++++++++++
.../sqlite-ast/class-wp-sqlite-driver.php | 7 +++++++
2 files changed, 24 insertions(+)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 075da64..1a22112 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -294,6 +294,23 @@ public function testDataTypes(): void {
);
}
+ public function testSystemVariables(): void {
+ $this->assertQuery(
+ 'SELECT NULL',
+ 'SELECT @@sql_mode'
+ );
+
+ $this->assertQuery(
+ 'SELECT NULL',
+ 'SELECT @@SESSION.sql_mode'
+ );
+
+ $this->assertQuery(
+ 'SELECT NULL',
+ 'SELECT @@GLOBAL.sql_mode'
+ );
+ }
+
private function assertQuery( $expected, string $query ): void {
$driver = new WP_SQLite_Driver( new PDO( 'sqlite::memory:' ) );
$driver->query( $query );
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index d1d4c21..349c8c0 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -862,6 +862,13 @@ private function translate( $ast ) {
throw $this->not_supported_exception(
sprintf( 'data type: %s', $child->value )
);
+ case 'systemVariable':
+ // @TODO: Emulate some system variables, or use reasonable defaults.
+ // See: https://dev.mysql.com/doc/refman/8.4/en/server-system-variable-reference.html
+ // See: https://dev.mysql.com/doc/refman/8.4/en/server-system-variables.html
+
+ // When we have no value, it's reasonable to use NULL.
+ return 'NULL';
default:
return $this->translate_sequence( $ast->get_children() );
}
From 70744b7d47b06dd3ee93f00508832c4be3508895 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 22 Nov 2024 11:33:05 +0100
Subject: [PATCH 011/124] Add support for UPDATE with ORDER BY and LIMIT
---
tests/WP_SQLite_Driver_Translation_Tests.php | 12 +++++
.../sqlite-ast/class-wp-sqlite-driver.php | 53 ++++++++++++++++++-
2 files changed, 63 insertions(+), 2 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 1a22112..1d7523b 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -160,6 +160,18 @@ public function testUpdate(): void {
'UPDATE "t" SET "c" = 1 WHERE "c" = 2',
'UPDATE t SET c = 1 WHERE c = 2'
);
+
+ // UPDATE with LIMIT.
+ $this->assertQuery(
+ 'UPDATE "t" SET "c" = 1 WHERE rowid IN ( SELECT rowid FROM "t" LIMIT 1 )',
+ 'UPDATE t SET c = 1 LIMIT 1'
+ );
+
+ // UPDATE with ORDER BY and LIMIT.
+ $this->assertQuery(
+ 'UPDATE "t" SET "c" = 1 WHERE rowid IN ( SELECT rowid FROM "t" ORDER BY "c" ASC LIMIT 1 )',
+ 'UPDATE t SET c = 1 ORDER BY c ASC LIMIT 1'
+ );
}
public function testDelete(): void {
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 349c8c0..e693894 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -760,12 +760,12 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
break;
case 'insertStatement':
case 'updateStatement':
+ $this->execute_update_statement( $ast );
+ break;
case 'replaceStatement':
case 'deleteStatement':
if ( 'insertStatement' === $ast->rule_name ) {
$this->query_type = 'INSERT';
- } elseif ( 'updateStatement' === $ast->rule_name ) {
- $this->query_type = 'UPDATE';
} elseif ( 'replaceStatement' === $ast->rule_name ) {
$this->query_type = 'REPLACE';
} elseif ( 'deleteStatement' === $ast->rule_name ) {
@@ -799,6 +799,52 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
}
}
+ private function execute_update_statement( WP_Parser_Node $node ): void {
+ // @TODO: Add support for UPDATE with multiple tables and JOINs.
+ // SQLite supports them in the FROM clause.
+
+ $has_order = $node->has_child_node( 'orderClause' );
+ $has_limit = $node->has_child_node( 'simpleLimitClause' );
+
+ /*
+ * SQLite doesn't support UPDATE with ORDER BY/LIMIT.
+ * We need to use a subquery to emulate this behavior.
+ *
+ * For instance, the following query:
+ * UPDATE t SET c = 1 WHERE c = 2 LIMIT 1;
+ * Will be rewritten to:
+ * UPDATE t SET c = 1 WHERE rowid IN ( SELECT rowid FROM t WHERE c = 2 LIMIT 1 );
+ */
+ if ( $has_order || $has_limit ) {
+ $subquery = 'SELECT rowid FROM ' . $this->translate_sequence(
+ array(
+ $node->get_descendant_node( 'tableReference' ),
+ $node->get_descendant_node( 'whereClause' ),
+ $node->get_descendant_node( 'orderClause' ),
+ $node->get_descendant_node( 'simpleLimitClause' ),
+ )
+ );
+
+ $update_nodes = array();
+ foreach ( $node->get_children() as $child ) {
+ $update_nodes[] = $child;
+ if (
+ $child instanceof WP_Parser_Node
+ && 'updateList' === $child->rule_name
+ ) {
+ // Skip WHERE, ORDER BY, and LIMIT.
+ break;
+ }
+ }
+ $query = $this->translate_sequence( $update_nodes )
+ . ' WHERE rowid IN ( ' . $subquery . ' )';
+ } else {
+ $query = $this->translate( $node );
+ }
+ $this->execute_sqlite_query( $query );
+ $this->set_result_from_affected_rows();
+ }
+
private function translate( $ast ) {
if ( null === $ast ) {
return null;
@@ -890,6 +936,9 @@ private function translate_token( WP_MySQL_Token $token ) {
private function translate_sequence( array $nodes, string $separator = ' ' ): string {
$parts = array();
foreach ( $nodes as $node ) {
+ if ( null === $node ) {
+ continue;
+ }
$parts[] = $this->translate( $node );
}
return implode( $separator, $parts );
From 797c3b78f4035f9da4262be75f55190e989a92a5 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 26 Nov 2024 18:24:34 +0100
Subject: [PATCH 012/124] Handle specifics of the CREATE TABLE statement
---
tests/WP_SQLite_Driver_Translation_Tests.php | 84 ++++++++-
.../sqlite-ast/class-wp-sqlite-driver.php | 165 +++++++++++++++++-
2 files changed, 239 insertions(+), 10 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 1d7523b..ca41765 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -15,10 +15,19 @@ class WP_SQLite_Driver_Translation_Tests extends TestCase {
*/
private static $grammar;
+ /**
+ * @var WP_SQLite_Driver
+ */
+ private $driver;
+
public static function setUpBeforeClass(): void {
self::$grammar = new WP_Parser_Grammar( include self::GRAMMAR_PATH );
}
+ public function setUp(): void {
+ $this->driver = new WP_SQLite_Driver( new PDO( 'sqlite::memory:' ) );
+ }
+
public function testSelect(): void {
$this->assertQuery(
'SELECT 1',
@@ -207,10 +216,74 @@ public function testCreateTable(): void {
'CREATE TABLE t (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT)'
);
+ // ENGINE is not supported in SQLite.
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "id" INTEGER )',
+ 'CREATE TABLE t (id INT) ENGINE=InnoDB'
+ );
+
+ /*
+ * PRIMARY KEY without AUTOINCREMENT:
+ * In this case, integer must be represented as INT, not INTEGER. SQLite
+ * treats "INTEGER PRIMARY KEY" as an alias for ROWID, causing unintended
+ * auto-increment-like behavior for a non-autoincrement column.
+ *
+ * See:
+ * https://www.sqlite.org/lang_createtable.html#rowids_and_the_integer_primary_key
+ */
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "id" INT PRIMARY KEY )',
+ 'CREATE TABLE t (id INT PRIMARY KEY)'
+ );
+
+ // With AUTOINCREMENT, we expect "INTEGER".
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT )',
+ 'CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT)'
+ );
+
+ // In SQLite, PRIMARY KEY must come before AUTOINCREMENT.
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT )',
+ 'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY)'
+ );
+
+ // In SQLite, AUTOINCREMENT cannot be specified separately from PRIMARY KEY.
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT )',
+ 'CREATE TABLE t (id INT AUTO_INCREMENT, PRIMARY KEY(id))'
+ );
+
+ // IF NOT EXISTS.
$this->assertQuery(
'CREATE TABLE IF NOT EXISTS "t" ( "id" INTEGER )',
'CREATE TABLE IF NOT EXISTS t (id INT)'
);
+
+ // CREATE TABLE AS SELECT ...
+ $this->assertQuery(
+ 'CREATE TABLE "t1" AS SELECT * FROM "t2"',
+ 'CREATE TABLE t1 AS SELECT * FROM t2'
+ );
+
+ // CREATE TABLE SELECT ...
+ // The "AS" keyword is optional in MySQL, but required in SQLite.
+ $this->assertQuery(
+ 'CREATE TABLE "t1" AS SELECT * FROM "t2"',
+ 'CREATE TABLE t1 SELECT * FROM t2'
+ );
+
+ // TEMPORARY.
+ $this->assertQuery(
+ 'CREATE TEMPORARY TABLE "t" ( "id" INTEGER )',
+ 'CREATE TEMPORARY TABLE t (id INT)'
+ );
+
+ // TEMPORARY & IF NOT EXISTS.
+ $this->assertQuery(
+ 'CREATE TEMPORARY TABLE IF NOT EXISTS "t" ( "id" INTEGER )',
+ 'CREATE TEMPORARY TABLE IF NOT EXISTS t (id INT)'
+ );
}
public function testDataTypes(): void {
@@ -324,18 +397,19 @@ public function testSystemVariables(): void {
}
private function assertQuery( $expected, string $query ): void {
- $driver = new WP_SQLite_Driver( new PDO( 'sqlite::memory:' ) );
- $driver->query( $query );
+ $this->driver->query( $query );
// Check for SQLite syntax errors.
// This ensures that invalid SQLite syntax will always fail, even if it
// was the expected result. It prevents us from using wrong assertions.
- $error = $driver->get_error_message();
+ $error = $this->driver->get_error_message();
if ( $error && preg_match( '/(SQLSTATE\[HY000].+syntax error\.)/i', $error, $matches ) ) {
- $this->fail( 'SQLite syntax error: ' . $matches[1] );
+ $this->fail(
+ sprintf( "SQLite syntax error: %s\nMySQL query: %s", $matches[1], $query )
+ );
}
- $executed_queries = array_column( $driver->executed_sqlite_queries, 'sql' );
+ $executed_queries = array_column( $this->driver->executed_sqlite_queries, 'sql' );
// Remove BEGIN and COMMIT/ROLLBACK queries.
if ( count( $executed_queries ) > 2 ) {
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index e693894..3858efa 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -780,9 +780,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$subtree = $ast->get_child_node();
switch ( $subtree->rule_name ) {
case 'createTable':
- $query = $this->translate( $ast );
- $this->execute_sqlite_query( $query );
- $this->set_result_from_affected_rows();
+ $this->execute_create_table_statement( $ast );
break;
default:
throw new Exception(
@@ -845,6 +843,97 @@ private function execute_update_statement( WP_Parser_Node $node ): void {
$this->set_result_from_affected_rows();
}
+ private function execute_create_table_statement( WP_Parser_Node $node ): void {
+ $element_list = $node->get_descendant_node( 'tableElementList' );
+ if ( null === $element_list ) {
+ $query = $this->translate( $node );
+ $this->execute_sqlite_query( $query );
+ $this->set_result_from_affected_rows();
+ return;
+ }
+
+ /*
+ * We need to handle some differences between MySQL and SQLite:
+ *
+ * 1. Inline index definitions:
+ *
+ * In MySQL, we can define an index inline with a column definition.
+ * In SQLite, we need to define indexes separately, using extra queries.
+ *
+ * 2. Column and constraint definition order:
+ *
+ * In MySQL, column and constraint definitions can be arbitrarily mixed.
+ * In SQLite, column definitions must come first, followed by constraints.
+ *
+ * 2. Auto-increment:
+ *
+ * In MySQL, there can at most one AUTO_INCREMENT column, and it must be
+ * a PRIMARY KEY, or the first column in a multi-column KEY.
+ *
+ * In SQLite, there can at most one AUTOINCREMENT column, and it must be
+ * a PRIMARY KEY, defined inline on a single column.
+ *
+ * Therefore, the following valid MySQL construct is not supported:
+ * CREATE TABLE t ( a INT AUTO_INCREMENT, b INT, PRIMARY KEY (a, b) );
+ * @TODO: Support it with a single-column PK and a multi-column UNIQUE KEY.
+ */
+
+ // Collect column, index, and constraint nodes.
+ $columns = array();
+ $constraints = array();
+ $indexes = array();
+ $has_autoincrement = false;
+ $primary_key_constraint = null; // Does not include inline PK definition.
+
+ foreach ( $element_list->get_descendant_nodes( 'columnDefinition' ) as $child ) {
+ if ( null !== $child->get_descendant_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) ) {
+ $has_autoincrement = true;
+ }
+ // @TODO: Collect inline index definitions.
+ $columns[] = $child;
+ }
+
+ foreach ( $element_list->get_descendant_nodes( 'tableConstraintDef' ) as $child ) {
+ if ( null !== $child->get_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) {
+ $primary_key_constraint = $child;
+ } else {
+ $constraints[] = $child;
+ }
+ }
+
+ /*
+ * If we have a PRIMARY KEY constraint:
+ * 1. Without auto-increment, we can put it back to the list of constraints.
+ * 2. With auto-increment, we need to later move it to the column definition.
+ */
+ if ( null !== $primary_key_constraint ) {
+ if ( ! $has_autoincrement ) {
+ $constraints[] = $primary_key_constraint;
+ } elseif ( count( $primary_key_constraint->get_descendant_nodes( 'keyPart' ) ) > 1 ) {
+ throw $this->not_supported_exception(
+ 'Composite primary key with AUTO_INCREMENT'
+ );
+ }
+ }
+
+ $query_parts = array( 'CREATE' );
+ foreach ( $node->get_child_node()->get_children() as $child ) {
+ if ( $child instanceof WP_Parser_Node && 'tableElementList' === $child->rule_name ) {
+ $query_parts[] = $this->translate_sequence( array_merge( $columns, $constraints ), ' , ' );
+ } else {
+ $part = $this->translate( $child );
+ if ( null !== $part ) {
+ $query_parts[] = $part;
+ }
+ }
+ }
+
+ // @TODO: Execute queries for inline index definitions.
+
+ $this->execute_sqlite_query( implode( ' ', $query_parts ) );
+ $this->set_result_from_affected_rows();
+ }
+
private function translate( $ast ) {
if ( null === $ast ) {
return null;
@@ -915,6 +1004,64 @@ private function translate( $ast ) {
// When we have no value, it's reasonable to use NULL.
return 'NULL';
+ case 'fieldDefinition':
+ /*
+ * In SQLite, there is the a quirk for backward compatibility:
+ * 1. INTEGER PRIMARY KEY creates an alias of ROWID.
+ * 2. INT PRIMARY KEY will not alias of ROWID.
+ *
+ * Therefore, we want to:
+ * 1. Use INTEGER PRIMARY KEY for when we have AUTOINCREMENT.
+ * 2. Use INT PRIMARY KEY otherwise.
+ */
+ $has_primary_key = $ast->get_descendant_token( WP_MySQL_Lexer::KEY_SYMBOL ) !== null;
+ $has_autoincrement = $ast->get_descendant_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) !== null;
+ $children = $ast->get_children();
+ $data_type_node = array_shift( $children );
+ $data_type = $this->translate( $data_type_node );
+ if ( $has_primary_key && 'INTEGER' === $data_type ) {
+ $data_type = $has_autoincrement ? 'INTEGER' : 'INT';
+ }
+
+ $attributes = $this->translate_sequence( $children );
+ $definition = $data_type . ( null === $attributes ? '' : " $attributes" );
+
+ /*
+ * In SQLite, AUTOINCREMENT must always be preceded by PRIMARY KEY.
+ * Therefore, we remove both PRIMARY KEY and AUTOINCREMENT from
+ * column attributes, and append them here in SQLite-friendly way.
+ */
+ if ( $has_autoincrement ) {
+ return $definition . ' PRIMARY KEY AUTOINCREMENT';
+ } elseif ( $has_primary_key ) {
+ return $definition . ' PRIMARY KEY';
+ }
+ return $definition;
+ case 'columnAttribute':
+ case 'gcolAttribute':
+ /*
+ * Remove PRIMARY KEY and AUTOINCREMENT from the column attributes.
+ * They are handled in the "fieldDefinition" node.
+ */
+ if ( $ast->has_child_token( WP_MySQL_Lexer::KEY_SYMBOL ) ) {
+ return null;
+ }
+ if ( $ast->has_child_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) ) {
+ return null;
+ }
+ return $this->translate_sequence( $ast->get_children() );
+ case 'createTableOptions':
+ return $this->translate_sequence( $ast->get_children(), ', ' );
+ case 'createTableOption':
+ if ( $ast->get_child_token( WP_MySQL_Lexer::ENGINE_SYMBOL ) ) {
+ return null;
+ }
+ return $this->translate_sequence( $ast->get_children() );
+ case 'duplicateAsQueryExpression':
+ // @TODO: How to handle IGNORE/REPLACE?
+
+ // The "AS" keyword is optional in MySQL, but required in SQLite.
+ return 'AS ' . $this->translate( $ast->get_child_node() );
default:
return $this->translate_sequence( $ast->get_children() );
}
@@ -933,13 +1080,21 @@ private function translate_token( WP_MySQL_Token $token ) {
}
}
- private function translate_sequence( array $nodes, string $separator = ' ' ): string {
+ private function translate_sequence( array $nodes, string $separator = ' ' ): ?string {
$parts = array();
foreach ( $nodes as $node ) {
if ( null === $node ) {
continue;
}
- $parts[] = $this->translate( $node );
+
+ $translated = $this->translate( $node );
+ if ( null === $translated ) {
+ continue;
+ }
+ $parts[] = $translated;
+ }
+ if ( 0 === count( $parts ) ) {
+ return null;
}
return implode( $separator, $parts );
}
From 647eaae62a9e82f09116c7f132021504b375c5fd Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Mon, 2 Dec 2024 17:04:33 +0100
Subject: [PATCH 013/124] Add basic ALTER TABLE support
---
tests/WP_SQLite_Driver_Translation_Tests.php | 49 ++++++++++
.../sqlite-ast/class-wp-sqlite-driver.php | 92 +++++++++++++++++++
2 files changed, 141 insertions(+)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index ca41765..1da6334 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -286,6 +286,55 @@ public function testCreateTable(): void {
);
}
+ public function testAlterTable(): void {
+ // Prepare a real table, so we can test multi-operation alter statements.
+ // Otherwise, we'd hit and exception and rollback after the first query.
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT )',
+ 'CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT)'
+ );
+
+ // ADD COLUMN.
+ $this->assertQuery(
+ 'ALTER TABLE "t" ADD COLUMN "a" INTEGER',
+ 'ALTER TABLE t ADD a INT'
+ );
+
+ // ADD COLUMN with multiple columns.
+ $this->assertQuery(
+ array(
+ 'ALTER TABLE "t" ADD COLUMN "b" INTEGER',
+ 'ALTER TABLE "t" ADD COLUMN "c" TEXT',
+ 'ALTER TABLE "t" ADD COLUMN "d" INTEGER',
+ ),
+ 'ALTER TABLE t ADD b INT, ADD c TEXT, ADD d BOOL'
+ );
+
+ // DROP COLUMN.
+ $this->assertQuery(
+ 'ALTER TABLE "t" DROP COLUMN "a"',
+ 'ALTER TABLE t DROP a'
+ );
+
+ // DROP COLUMN with multiple columns.
+ $this->assertQuery(
+ array(
+ 'ALTER TABLE "t" DROP COLUMN "b"',
+ 'ALTER TABLE "t" DROP COLUMN "c"',
+ ),
+ 'ALTER TABLE t DROP b, DROP c'
+ );
+
+ // ADD COLUMN and DROP COLUMN combined.
+ $this->assertQuery(
+ array(
+ 'ALTER TABLE "t" ADD COLUMN "a" INTEGER',
+ 'ALTER TABLE "t" DROP COLUMN "d"',
+ ),
+ 'ALTER TABLE t ADD a INT, DROP d'
+ );
+ }
+
public function testDataTypes(): void {
// Numeric data types.
$this->assertQuery(
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 3858efa..0cb250a 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -792,6 +792,36 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
);
}
break;
+ case 'alterStatement':
+ $this->query_type = 'ALTER';
+ $subtree = $ast->get_child_node();
+ switch ( $subtree->rule_name ) {
+ case 'alterTable':
+ $this->execute_alter_table_statement( $ast );
+ break;
+ default:
+ throw new Exception(
+ sprintf(
+ 'Unsupported statement type: "%s" > "%s"',
+ $ast->rule_name,
+ $subtree->rule_name
+ )
+ );
+ }
+ break;
+ case 'dropStatement':
+ $this->query_type = 'DROP';
+ $query = $this->translate( $ast );
+ $this->execute_sqlite_query( $query );
+ $this->set_result_from_affected_rows();
+ break;
+ case 'setStatement':
+ /*
+ * It would be lovely to support at least SET autocommit,
+ * but I don't think that is even possible with SQLite.
+ */
+ $this->results = 0;
+ break;
default:
throw new Exception( sprintf( 'Unsupported statement type: "%s"', $ast->rule_name ) );
}
@@ -934,6 +964,68 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void {
$this->set_result_from_affected_rows();
}
+ private function execute_alter_table_statement( WP_Parser_Node $node ): void {
+ $table_name = $this->translate( $node->get_descendant_node( 'tableRef' ) );
+ $actions = $node->get_descendant_nodes( 'alterListItem' );
+
+ /*
+ * SQLite supports only a small subset of MySQL ALTER TABLE statement.
+ * We need to handle some differences and emulate some operations:
+ *
+ * 1. Multiple operations in a single ALTER TABLE statement.
+ *
+ * SQLite doesn't support multiple operations in a single ALTER TABLE
+ * statement. We need to execute each operation as a separate query.
+ *
+ * 2. ADD COLUMN in SQLite doesn't support some valid MySQL constructs:
+ *
+ * - Adding a column with PRIMARY KEY or UNIQUE constraint.
+ * - Adding a column with AUTOINCREMENT.
+ * - Adding a column with CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP,
+ * or an expression in parentheses as a default value.
+ * - Adding a NOT NULL column without a default value when the table is
+ * not empty. In MySQL, this depends on the data type and SQL mode.
+ *
+ * @TODO: Address these nuances.
+ */
+ foreach ( $actions as $action ) {
+ $token = $action->get_child_token();
+
+ // ADD column/constraint.
+ if ( WP_MySQL_Lexer::ADD_SYMBOL === $token->id ) {
+ // ADD COLUMN.
+ $field_definition = $action->get_descendant_node( 'fieldDefinition' );
+ if ( null !== $field_definition ) {
+ $field_name = $this->translate( $action->get_child_node( 'identifier' ) );
+ $field = $this->translate( $field_definition );
+ $this->execute_sqlite_query(
+ sprintf( 'ALTER TABLE %s ADD COLUMN %s %s', $table_name, $field_name, $field )
+ );
+ }
+
+ // ADD CONSTRAINT.
+ $constraint = $action->get_descendant_node( 'tableConstraintDef' );
+ if ( null !== $constraint ) {
+ $constraint_name = $this->translate( $constraint->get_child_node( 'identifier' ) );
+ $constraint = $this->translate( $constraint );
+ $this->execute_sqlite_query(
+ sprintf( 'ALTER TABLE %s ADD CONSTRAINT %s %s', $table_name, $constraint_name, $constraint )
+ );
+ }
+ } elseif ( WP_MySQL_Lexer::DROP_SYMBOL === $token->id ) {
+ // DROP COLUMN.
+ $field_name = $this->translate( $action->get_child_node( 'columnInternalRef' ) );
+ if ( null !== $field_name ) {
+ $this->execute_sqlite_query(
+ sprintf( 'ALTER TABLE %s DROP COLUMN %s', $table_name, $field_name )
+ );
+ }
+ }
+ }
+
+ $this->set_result_from_affected_rows();
+ }
+
private function translate( $ast ) {
if ( null === $ast ) {
return null;
From c5156054c23790e50e86574068d4721d52bbc13e Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Mon, 2 Dec 2024 17:18:01 +0100
Subject: [PATCH 014/124] Fix MySQL syntax errors in tests
---
tests/WP_SQLite_Driver_Tests.php | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index bee50ae..65c3611 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -247,7 +247,7 @@ public function testUpdateWithoutWhereButWithLimit() {
public function testCastAsBinary() {
$this->assertQuery(
// Use a confusing alias to make sure it replaces only the correct token
- "SELECT CAST('ABC' AS BINARY) as binary;"
+ "SELECT CAST('ABC' AS BINARY) as `binary`;"
);
$results = $this->engine->get_query_results();
$this->assertCount( 1, $results );
@@ -470,7 +470,7 @@ public function testShowCreateTableWithCorrectDefaultValues() {
"CREATE TABLE _tmp__table (
ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL,
default_empty_string VARCHAR(255) default '',
- null_no_default VARCHAR(255),
+ null_no_default VARCHAR(255)
);"
);
@@ -596,7 +596,7 @@ public function testShowTableStatusFrom() {
);
$this->assertQuery(
- "SHOW TABLE STATUS FROM 'mydb';"
+ 'SHOW TABLE STATUS FROM mydb;'
);
$this->assertCount(
@@ -619,7 +619,7 @@ public function testShowTableStatusIn() {
);
$this->assertQuery(
- "SHOW TABLE STATUS IN 'mydb';"
+ 'SHOW TABLE STATUS IN mydb;'
);
$this->assertCount(
@@ -649,7 +649,7 @@ public function testShowTableStatusInTwoTables() {
);"
);
$this->assertQuery(
- "SHOW TABLE STATUS IN 'mydb';"
+ 'SHOW TABLE STATUS IN mydb;'
);
$this->assertCount(
@@ -807,7 +807,7 @@ public function testCreateTableWithTrailingComma() {
$result = $this->assertQuery(
'CREATE TABLE wptests_users (
ID bigint(20) unsigned NOT NULL auto_increment,
- PRIMARY KEY (ID),
+ PRIMARY KEY (ID)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci'
);
$this->assertEquals( '', $this->engine->get_error_message() );
@@ -818,7 +818,7 @@ public function testCreateTableSpatialIndex() {
$result = $this->assertQuery(
'CREATE TABLE wptests_users (
ID bigint(20) unsigned NOT NULL auto_increment,
- UNIQUE KEY (ID),
+ UNIQUE KEY (ID)
)'
);
$this->assertEquals( '', $this->engine->get_error_message() );
@@ -832,7 +832,7 @@ public function testCreateTableWithMultiValueColumnTypeModifiers() {
decimal_column DECIMAL(10,2) NOT NULL DEFAULT 0,
float_column FLOAT(10,2) NOT NULL DEFAULT 0,
enum_column ENUM('a', 'b', 'c') NOT NULL DEFAULT 'a',
- PRIMARY KEY (ID),
+ PRIMARY KEY (ID)
)"
);
$this->assertEquals( '', $this->engine->get_error_message() );
@@ -2716,7 +2716,7 @@ public function testZeroPlusStringToFloatComparison() {
public function testCalcFoundRows() {
$result = $this->assertQuery(
"CREATE TABLE wptests_dummy (
- ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
user_login TEXT NOT NULL default ''
);"
);
@@ -3030,7 +3030,7 @@ public function testTranslateLikeBinaryAndGlob() {
// Create a temporary table for testing
$this->assertQuery(
"CREATE TABLE _tmp_table (
- ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
name varchar(20) NOT NULL default ''
);"
);
@@ -3114,7 +3114,7 @@ public function testTranslateLikeBinaryAndGlob() {
public function testOnConflictReplace() {
$this->assertQuery(
"CREATE TABLE _tmp_table (
- ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
name varchar(20) NOT NULL default 'default-value',
unique_name varchar(20) NOT NULL default 'unique-default-value',
inline_unique_name varchar(20) NOT NULL default 'inline-unique-default-value',
From 1e5c3118f93c50e4e83af7aa6b2fb2f864e0e1d6 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 19 Dec 2024 10:11:38 +0100
Subject: [PATCH 015/124] Introduce information schema builder & create
information schema tables
---
tests/WP_SQLite_Driver_Tests.php | 3 +-
.../sqlite-ast/class-wp-sqlite-driver.php | 24 +-
...s-wp-sqlite-information-schema-builder.php | 250 ++++++++++++++++++
3 files changed, 274 insertions(+), 3 deletions(-)
create mode 100644 wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 65c3611..cd3c3ed 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -2,6 +2,7 @@
require_once __DIR__ . '/WP_SQLite_Translator_Tests.php';
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
+require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-expression.php';
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-token-factory.php';
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-token.php';
@@ -39,7 +40,7 @@ public static function setUpBeforeClass(): void {
public function setUp(): void {
$this->sqlite = new PDO( 'sqlite::memory:' );
- $this->engine = new WP_SQLite_Driver( $this->sqlite );
+ $this->engine = new WP_SQLite_Driver( 'wp', $this->sqlite );
$this->engine->query(
"CREATE TABLE _options (
ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 0cb250a..7bd37d0 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -94,6 +94,13 @@ class WP_SQLite_Driver {
*/
private static $grammar;
+ /**
+ * The database name. In WordPress, the value of DB_NAME.
+ *
+ * @var string
+ */
+ private $db_name;
+
/**
* Class variable to reference to the PDO instance.
*
@@ -247,6 +254,11 @@ class WP_SQLite_Driver {
*/
private $last_sqlite_error;
+ /**
+ * @var WP_SQLite_Information_Schema_Builder
+ */
+ private $information_schema_builder;
+
/**
* Constructor.
*
@@ -254,9 +266,11 @@ class WP_SQLite_Driver {
* Don't use parent::__construct() because this class does not only returns
* PDO instance but many others jobs.
*
- * @param PDO $pdo The PDO object.
+ * @param string $db_name The database name. In WordPress, the value of DB_NAME.
+ * @param PDO|null $pdo The PDO object.
*/
- public function __construct( $pdo = null ) {
+ public function __construct( string $db_name, ?PDO $pdo = null ) {
+ $this->db_name = $db_name;
if ( ! $pdo ) {
if ( ! is_file( FQDB ) ) {
$this->prepare_directory();
@@ -313,6 +327,12 @@ public function __construct( $pdo = null ) {
$this->pdo = $pdo;
+ $this->information_schema_builder = new WP_SQLite_Information_Schema_Builder(
+ $this->db_name,
+ array( $this, 'execute_sqlite_query' )
+ );
+ $this->information_schema_builder->ensure_information_schema_tables();
+
// Load MySQL grammar.
if ( null === self::$grammar ) {
self::$grammar = new WP_Parser_Grammar( require self::GRAMMAR_PATH );
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
new file mode 100644
index 0000000..981529a
--- /dev/null
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -0,0 +1,250 @@
+db_name = $db_name;
+ $this->query_callback = $query_callback;
+ }
+
+ /**
+ * Ensure that the supported information schema tables exist in the SQLite
+ * database. Tables that are missing will be created.
+ */
+ public function ensure_information_schema_tables(): void {
+ foreach ( self::CREATE_INFORMATION_SCHEMA_QUERIES as $query ) {
+ $this->query( $query );
+ }
+ }
+
+ /**
+ * @param string $query
+ * @param array $params
+ * @return PDOStatement
+ */
+ private function query( string $query, array $params = array() ) {
+ return ( $this->query_callback )( $query, $params );
+ }
+}
From b617ef9a74381486ab39674214b5c14e282ff0c0 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 19 Dec 2024 10:49:21 +0100
Subject: [PATCH 016/124] Record CREATE TABLE table info in information schema
---
...s-wp-sqlite-information-schema-builder.php | 147 +++++++++++++++---
1 file changed, 126 insertions(+), 21 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index 981529a..f31779d 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -4,6 +4,8 @@
namespace WIP;
use PDOStatement;
+use WP_MySQL_Lexer;
+use WP_Parser_Node;
class WP_SQLite_Information_Schema_Builder {
/**
@@ -28,28 +30,28 @@ class WP_SQLite_Information_Schema_Builder {
const CREATE_INFORMATION_SCHEMA_QUERIES = array(
// TABLES
"CREATE TABLE IF NOT EXISTS _mysql_information_schema_tables (
- TABLE_CATALOG TEXT NOT NULL DEFAULT 'def',
- TABLE_SCHEMA TEXT NOT NULL,
- TABLE_NAME TEXT NOT NULL,
- TABLE_TYPE TEXT NOT NULL,
- ENGINE TEXT NOT NULL,
- VERSION INTEGER NOT NULL DEFAULT 10,
- ROW_FORMAT TEXT NOT NULL,
- TABLE_ROWS INTEGER NOT NULL DEFAULT 0,
- AVG_ROW_LENGTH INTEGER NOT NULL DEFAULT 0,
- DATA_LENGTH INTEGER NOT NULL DEFAULT 0,
- MAX_DATA_LENGTH INTEGER NOT NULL DEFAULT 0,
- INDEX_LENGTH INTEGER NOT NULL DEFAULT 0,
- DATA_FREE INTEGER NOT NULL DEFAULT 0,
- AUTO_INCREMENT INTEGER,
- CREATE_TIME TEXT NOT NULL
+ TABLE_CATALOG TEXT NOT NULL DEFAULT 'def', -- always 'def'
+ TABLE_SCHEMA TEXT NOT NULL, -- database name
+ TABLE_NAME TEXT NOT NULL, -- table name
+ TABLE_TYPE TEXT NOT NULL, -- 'BASE TABLE' or 'VIEW'
+ ENGINE TEXT NOT NULL, -- storage engine
+ VERSION INTEGER NOT NULL DEFAULT 10, -- unused, in MySQL 8 hardcoded to 10
+ ROW_FORMAT TEXT NOT NULL, -- row storage format @TODO - implement
+ TABLE_ROWS INTEGER NOT NULL DEFAULT 0, -- not implemented
+ AVG_ROW_LENGTH INTEGER NOT NULL DEFAULT 0, -- not implemented
+ DATA_LENGTH INTEGER NOT NULL DEFAULT 0, -- not implemented
+ MAX_DATA_LENGTH INTEGER NOT NULL DEFAULT 0, -- not implemented
+ INDEX_LENGTH INTEGER NOT NULL DEFAULT 0, -- not implemented
+ DATA_FREE INTEGER NOT NULL DEFAULT 0, -- not implemented
+ AUTO_INCREMENT INTEGER, -- not implemented
+ CREATE_TIME TEXT NOT NULL -- table creation timestamp
DEFAULT CURRENT_TIMESTAMP,
- UPDATE_TIME TEXT,
- CHECK_TIME TEXT,
- TABLE_COLLATION TEXT NOT NULL,
- CHECKSUM INTEGER,
- CREATE_OPTIONS TEXT,
- TABLE_COMMENT TEXT NOT NULL DEFAULT ''
+ UPDATE_TIME TEXT, -- table update time
+ CHECK_TIME TEXT, -- not implemented
+ TABLE_COLLATION TEXT NOT NULL, -- table collation
+ CHECKSUM INTEGER, -- not implemented
+ CREATE_OPTIONS TEXT, -- extra CREATE TABLE options
+ TABLE_COMMENT TEXT NOT NULL DEFAULT '' -- comment
) STRICT",
// COLUMNS
@@ -239,6 +241,109 @@ public function ensure_information_schema_tables(): void {
}
}
+ /**
+ * Analyze CREATE TABLE statement and record data in the information schema.
+ *
+ * @param WP_Parser_Node $node AST node representing a CREATE TABLE statement.
+ */
+ public function record_create_table( WP_Parser_Node $node ): void {
+ $table_name = $this->get_value( $node->get_descendant_node( 'tableName' ) );
+ $table_engine = $this->get_table_engine( $node );
+ $table_row_format = 'MyISAM' === $table_engine ? 'FIXED' : 'DYNAMIC';
+ $table_collation = $this->get_table_collation( $node );
+
+ // 1. Table.
+ $this->insert_values(
+ '_mysql_information_schema_tables',
+ array(
+ 'table_schema' => $this->db_name,
+ 'table_name' => $table_name,
+ 'table_type' => 'BASE TABLE',
+ 'engine' => $table_engine,
+ 'row_format' => $table_row_format,
+ 'table_collation' => $table_collation,
+ )
+ );
+ }
+
+ private function get_table_engine( WP_Parser_Node $node ): string {
+ $engine_node = $node->get_descendant_node( 'engineRef' );
+ if ( null === $engine_node ) {
+ return 'InnoDB';
+ }
+
+ $engine = strtoupper( $this->get_value( $engine_node ) );
+ if ( 'INNODB' === $engine ) {
+ return 'InnoDB';
+ } elseif ( 'MYISAM' === $engine ) {
+ return 'MyISAM';
+ }
+ return $engine;
+ }
+
+ private function get_table_collation( WP_Parser_Node $node ): string {
+ $collate_node = $node->get_descendant_node( 'collationName' );
+ if ( null === $collate_node ) {
+ // @TODO: Use default DB collation or DB_CHARSET & DB_COLLATE.
+ return 'utf8mb4_general_ci';
+ }
+ return strtolower( $this->get_value( $collate_node ) );
+ }
+
+ /**
+ * This is a helper function to get the full unescaped value of a node.
+ *
+ * @TODO: This should be done in a more correct way, for names maybe allowing
+ * descending only a single-child hierarchy, such as these:
+ * identifier -> pureIdentifier -> IDENTIFIER
+ * identifier -> pureIdentifier -> BACKTICK_QUOTED_ID
+ * identifier -> pureIdentifier -> DOUBLE_QUOTED_TEXT
+ * etc.
+ *
+ * For saving "DEFAULT ..." in column definitions, we actually need to
+ * serialize the whole node, in the case of expressions. This may mean
+ * implementing an MySQL AST -> string printer.
+ *
+ * @param WP_Parser_Node $node
+ * @return string
+ */
+ private function get_value( WP_Parser_Node $node ): string {
+ $full_value = '';
+ foreach ( $node->get_children() as $child ) {
+ if ( $child instanceof WP_Parser_Node ) {
+ $value = $this->get_value( $child );
+ } elseif ( WP_MySQL_Lexer::BACK_TICK_QUOTED_ID === $child->id ) {
+ $value = substr( $child->value, 1, -1 );
+ $value = str_replace( '\`', '`', $value );
+ $value = str_replace( '``', '`', $value );
+ } elseif ( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT === $child->id ) {
+ $value = $child->value;
+ $value = substr( $value, 1, -1 );
+ $value = str_replace( '\"', '"', $value );
+ $value = str_replace( '""', '"', $value );
+ } elseif ( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT === $child->id ) {
+ $value = $child->value;
+ $value = substr( $value, 1, -1 );
+ $value = str_replace( '\"', '"', $value );
+ $value = str_replace( '""', '"', $value );
+ } else {
+ $value = $child->value;
+ }
+ $full_value .= $value;
+ }
+ return $full_value;
+ }
+
+ private function insert_values( string $table_name, array $data ): void {
+ $this->query(
+ '
+ INSERT INTO ' . $table_name . ' (' . implode( ', ', array_keys( $data ) ) . ')
+ VALUES (' . implode( ', ', array_fill( 0, count( $data ), '?' ) ) . ')
+ ',
+ array_values( $data )
+ );
+ }
+
/**
* @param string $query
* @param array $params
From dbd50e60a48eff52ca187259b7a4f3590edafb2c Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 19 Dec 2024 14:17:02 +0100
Subject: [PATCH 017/124] Record CREATE TABLE column info in information schema
---
...s-wp-sqlite-information-schema-builder.php | 650 +++++++++++++++++-
1 file changed, 628 insertions(+), 22 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index f31779d..419fa80 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -56,28 +56,28 @@ class WP_SQLite_Information_Schema_Builder {
// COLUMNS
"CREATE TABLE IF NOT EXISTS _mysql_information_schema_columns (
- TABLE_CATALOG TEXT NOT NULL DEFAULT 'def',
- TABLE_SCHEMA TEXT NOT NULL,
- TABLE_NAME TEXT NOT NULL,
- COLUMN_NAME TEXT NOT NULL,
- ORDINAL_POSITION INTEGER NOT NULL,
- COLUMN_DEFAULT TEXT,
- IS_NULLABLE TEXT NOT NULL,
- DATA_TYPE TEXT NOT NULL,
- CHARACTER_MAXIMUM_LENGTH INTEGER,
- CHARACTER_OCTET_LENGTH INTEGER,
- NUMERIC_PRECISION INTEGER,
- NUMERIC_SCALE INTEGER,
- DATETIME_PRECISION INTEGER,
- CHARACTER_SET_NAME TEXT,
- COLLATION_NAME TEXT,
- COLUMN_TYPE TEXT NOT NULL,
- COLUMN_KEY TEXT NOT NULL DEFAULT '',
- EXTRA TEXT NOT NULL DEFAULT '',
- PRIVILEGES TEXT NOT NULL,
- COLUMN_COMMENT TEXT NOT NULL DEFAULT '',
- GENERATION_EXPRESSION TEXT NOT NULL DEFAULT '',
- SRS_ID INTEGER
+ TABLE_CATALOG TEXT NOT NULL DEFAULT 'def', -- always 'def'
+ TABLE_SCHEMA TEXT NOT NULL, -- database name
+ TABLE_NAME TEXT NOT NULL, -- table name
+ COLUMN_NAME TEXT NOT NULL, -- column name
+ ORDINAL_POSITION INTEGER NOT NULL, -- column position
+ COLUMN_DEFAULT TEXT, -- default value, NULL for both NULL and none
+ IS_NULLABLE TEXT NOT NULL, -- 'YES' or 'NO'
+ DATA_TYPE TEXT NOT NULL, -- data type (without length, precision, etc.)
+ CHARACTER_MAXIMUM_LENGTH INTEGER, -- max length for string columns in characters
+ CHARACTER_OCTET_LENGTH INTEGER, -- max length for string columns in bytes
+ NUMERIC_PRECISION INTEGER, -- number precision for numeric columns
+ NUMERIC_SCALE INTEGER, -- number scale for numeric columns
+ DATETIME_PRECISION INTEGER, -- fractional seconds precision for temporal columns
+ CHARACTER_SET_NAME TEXT, -- charset for string columns
+ COLLATION_NAME TEXT, -- collation for string columns
+ COLUMN_TYPE TEXT NOT NULL, -- full data type (with length, precision, etc.)
+ COLUMN_KEY TEXT NOT NULL DEFAULT '', -- if column is indexed ('', 'PRI', 'UNI', 'MUL')
+ EXTRA TEXT NOT NULL DEFAULT '', -- AUTO_INCREMENT, VIRTUAL, STORED, etc.
+ PRIVILEGES TEXT NOT NULL, -- not implemented
+ COLUMN_COMMENT TEXT NOT NULL DEFAULT '', -- comment
+ GENERATION_EXPRESSION TEXT NOT NULL DEFAULT '', -- expression for generated columns
+ SRS_ID INTEGER -- not implemented
) STRICT",
// VIEWS
@@ -199,6 +199,125 @@ class WP_SQLite_Information_Schema_Builder {
) STRICT',
);
+ /**
+ * A mapping of MySQL tokens to normalized MySQL data types.
+ * This is used to store column data types in the information schema.
+ */
+ const TOKEN_TO_TYPE_MAP = array(
+ WP_MySQL_Lexer::INT_SYMBOL => 'int',
+ WP_MySQL_Lexer::TINYINT_SYMBOL => 'tinyint',
+ WP_MySQL_Lexer::SMALLINT_SYMBOL => 'smallint',
+ WP_MySQL_Lexer::MEDIUMINT_SYMBOL => 'mediumint',
+ WP_MySQL_Lexer::BIGINT_SYMBOL => 'bigint',
+ WP_MySQL_Lexer::REAL_SYMBOL => 'double',
+ WP_MySQL_Lexer::DOUBLE_SYMBOL => 'double',
+ WP_MySQL_Lexer::FLOAT_SYMBOL => 'float',
+ WP_MySQL_Lexer::DECIMAL_SYMBOL => 'decimal',
+ WP_MySQL_Lexer::NUMERIC_SYMBOL => 'decimal',
+ WP_MySQL_Lexer::FIXED_SYMBOL => 'decimal',
+ WP_MySQL_Lexer::BIT_SYMBOL => 'bit',
+ WP_MySQL_Lexer::BOOL_SYMBOL => 'tinyint',
+ WP_MySQL_Lexer::BOOLEAN_SYMBOL => 'tinyint',
+ WP_MySQL_Lexer::BINARY_SYMBOL => 'binary',
+ WP_MySQL_Lexer::VARBINARY_SYMBOL => 'varbinary',
+ WP_MySQL_Lexer::YEAR_SYMBOL => 'year',
+ WP_MySQL_Lexer::DATE_SYMBOL => 'date',
+ WP_MySQL_Lexer::TIME_SYMBOL => 'time',
+ WP_MySQL_Lexer::TIMESTAMP_SYMBOL => 'timestamp',
+ WP_MySQL_Lexer::DATETIME_SYMBOL => 'datetime',
+ WP_MySQL_Lexer::TINYBLOB_SYMBOL => 'tinyblob',
+ WP_MySQL_Lexer::BLOB_SYMBOL => 'blob',
+ WP_MySQL_Lexer::MEDIUMBLOB_SYMBOL => 'mediumblob',
+ WP_MySQL_Lexer::LONGBLOB_SYMBOL => 'longblob',
+ WP_MySQL_Lexer::TINYTEXT_SYMBOL => 'tinytext',
+ WP_MySQL_Lexer::TEXT_SYMBOL => 'text',
+ WP_MySQL_Lexer::MEDIUMTEXT_SYMBOL => 'mediumtext',
+ WP_MySQL_Lexer::LONGTEXT_SYMBOL => 'longtext',
+ WP_MySQL_Lexer::ENUM_SYMBOL => 'enum',
+ WP_MySQL_Lexer::SET_SYMBOL => 'set',
+ WP_MySQL_Lexer::SERIAL_SYMBOL => 'bigint',
+ WP_MySQL_Lexer::GEOMETRY_SYMBOL => 'geometry',
+ WP_MySQL_Lexer::GEOMETRYCOLLECTION_SYMBOL => 'geomcollection',
+ WP_MySQL_Lexer::POINT_SYMBOL => 'point',
+ WP_MySQL_Lexer::MULTIPOINT_SYMBOL => 'multipoint',
+ WP_MySQL_Lexer::LINESTRING_SYMBOL => 'linestring',
+ WP_MySQL_Lexer::MULTILINESTRING_SYMBOL => 'multilinestring',
+ WP_MySQL_Lexer::POLYGON_SYMBOL => 'polygon',
+ WP_MySQL_Lexer::MULTIPOLYGON_SYMBOL => 'multipolygon',
+ WP_MySQL_Lexer::JSON_SYMBOL => 'json',
+ );
+
+ /**
+ * The default collation for each MySQL charset.
+ * This is needed as collation is not always specified in a query.
+ */
+ const CHARSET_DEFAULT_COLLATION_MAP = array(
+ 'armscii8' => 'armscii8_general_ci',
+ 'ascii' => 'ascii_general_ci',
+ 'big5' => 'big5_chinese_ci',
+ 'binary' => 'binary',
+ 'cp1250' => 'cp1250_general_ci',
+ 'cp1251' => 'cp1251_general_ci',
+ 'cp1256' => 'cp1256_general_ci',
+ 'cp1257' => 'cp1257_general_ci',
+ 'cp850' => 'cp850_general_ci',
+ 'cp852' => 'cp852_general_ci',
+ 'cp866' => 'cp866_general_ci',
+ 'cp932' => 'cp932_japanese_ci',
+ 'dec8' => 'dec8_swedish_ci',
+ 'eucjpms' => 'eucjpms_japanese_ci',
+ 'euckr' => 'euckr_korean_ci',
+ 'gb18030' => 'gb18030_chinese_ci',
+ 'gb2312' => 'gb2312_chinese_ci',
+ 'gbk' => 'gbk_chinese_ci',
+ 'geostd8' => 'geostd8_general_ci',
+ 'greek' => 'greek_general_ci',
+ 'hebrew' => 'hebrew_general_ci',
+ 'hp8' => 'hp8_english_ci',
+ 'keybcs2' => 'keybcs2_general_ci',
+ 'koi8r' => 'koi8r_general_ci',
+ 'koi8u' => 'koi8u_general_ci',
+ 'latin1' => 'latin1_swedish_ci',
+ 'latin2' => 'latin2_general_ci',
+ 'latin5' => 'latin5_turkish_ci',
+ 'latin7' => 'latin7_general_ci',
+ 'macce' => 'macce_general_ci',
+ 'macroman' => 'macroman_general_ci',
+ 'sjis' => 'sjis_japanese_ci',
+ 'swe7' => 'swe7_swedish_ci',
+ 'tis620' => 'tis620_thai_ci',
+ 'ucs2' => 'ucs2_general_ci',
+ 'ujis' => 'ujis_japanese_ci',
+ 'utf16' => 'utf16_general_ci',
+ 'utf16le' => 'utf16le_general_ci',
+ 'utf32' => 'utf32_general_ci',
+ 'utf8' => 'utf8_general_ci',
+ 'utf8mb4' => 'utf8mb4_general_ci', // @TODO: From MySQL 8.0.1, this is utf8mb4_0900_ai_ci.
+ );
+
+ /**
+ * Maximum number of bytes per character for each charset.
+ * The map contains only multi-byte charsets.
+ * Charsets that are not included are single-byte.
+ */
+ const CHARSET_MAX_BYTES_MAP = array(
+ 'big5' => 2,
+ 'cp932' => 2,
+ 'eucjpms' => 3,
+ 'euckr' => 2,
+ 'gb18030' => 4,
+ 'gb2312' => 2,
+ 'gbk' => 2,
+ 'sjis' => 2,
+ 'ucs2' => 2,
+ 'ujis' => 3,
+ 'utf16' => 4,
+ 'utf16le' => 4,
+ 'utf32' => 4,
+ 'utf8' => 3,
+ 'utf8mb4' => 4,
+ );
+
/**
* Database name.
*
@@ -264,6 +383,61 @@ public function record_create_table( WP_Parser_Node $node ): void {
'table_collation' => $table_collation,
)
);
+
+ // 2. Columns.
+ $column_position = 1;
+ foreach ( $node->get_descendant_nodes( 'columnDefinition' ) as $column_node ) {
+ $column_name = $this->get_value( $column_node->get_child_node( 'columnName' ) );
+
+ // Column definition.
+ $column_data = $this->extract_column_data(
+ $table_name,
+ $column_name,
+ $column_node,
+ $column_position
+ );
+ $this->insert_values( '_mysql_information_schema_columns', $column_data );
+ $column_position += 1;
+ }
+ }
+
+ private function extract_column_data( string $table_name, string $column_name, WP_Parser_Node $node, int $position ): array {
+ $default = $this->get_column_default( $node );
+ $nullable = $this->get_column_nullable( $node );
+ $key = $this->get_column_key( $node );
+ $extra = $this->get_column_extra( $node );
+ $comment = $this->get_column_comment( $node );
+
+ list ( $data_type, $column_type ) = $this->get_column_data_types( $node );
+ list ( $charset, $collation ) = $this->get_column_charset_and_collation( $node, $data_type );
+ list ( $char_length, $octet_length ) = $this->get_column_lengths( $node, $data_type, $charset );
+ list ( $precision, $scale ) = $this->get_column_numeric_attributes( $node, $data_type );
+ $datetime_precision = $this->get_column_datetime_precision( $node, $data_type );
+ $generation_expression = $this->get_column_generation_expression( $node );
+
+ return array(
+ 'table_schema' => $this->db_name,
+ 'table_name' => $table_name,
+ 'column_name' => $column_name,
+ 'ordinal_position' => $position,
+ 'column_default' => $default,
+ 'is_nullable' => $nullable,
+ 'data_type' => $data_type,
+ 'character_maximum_length' => $char_length,
+ 'character_octet_length' => $octet_length,
+ 'numeric_precision' => $precision,
+ 'numeric_scale' => $scale,
+ 'datetime_precision' => $datetime_precision,
+ 'character_set_name' => $charset,
+ 'collation_name' => $collation,
+ 'column_type' => $column_type,
+ 'column_key' => $key,
+ 'extra' => $extra,
+ 'privileges' => 'select,insert,update,references',
+ 'column_comment' => $comment,
+ 'generation_expression' => $generation_expression,
+ 'srs_id' => null, // not implemented
+ );
}
private function get_table_engine( WP_Parser_Node $node ): string {
@@ -290,6 +464,438 @@ private function get_table_collation( WP_Parser_Node $node ): string {
return strtolower( $this->get_value( $collate_node ) );
}
+ private function get_column_default( WP_Parser_Node $node ): ?string {
+ foreach ( $node->get_descendant_nodes( 'columnAttribute' ) as $attr ) {
+ if ( $attr->has_child_token( WP_MySQL_Lexer::DEFAULT_SYMBOL ) ) {
+ // @TODO: MySQL seems to normalize default values for numeric
+ // columns, such as 1.0 to 1, 1e3 to 1000, etc.
+ return substr( $this->get_value( $attr ), strlen( 'DEFAULT' ) );
+ }
+ }
+ return null;
+ }
+
+ private function get_column_nullable( WP_Parser_Node $node ): string {
+ // SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
+ $data_type = $node->get_descendant_node( 'dataType' );
+ if ( null !== $data_type->get_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) {
+ return 'NO';
+ }
+
+ foreach ( $node->get_descendant_nodes( 'columnAttribute' ) as $attr ) {
+ // PRIMARY KEY columns are always NOT NULL.
+ if ( $attr->has_child_token( WP_MySQL_Lexer::KEY_SYMBOL ) ) {
+ return 'NO';
+ }
+
+ // Check for NOT NULL attribute.
+ if (
+ $attr->has_child_token( WP_MySQL_Lexer::NOT_SYMBOL )
+ && $attr->has_child_node( 'nullLiteral' )
+ ) {
+ return 'NO';
+ }
+ }
+ return 'YES';
+ }
+
+ private function get_column_key( WP_Parser_Node $column_node ): string {
+ // 1. PRI: Column is a primary key or its any component.
+ if (
+ null !== $column_node->get_descendant_token( WP_MySQL_Lexer::KEY_SYMBOL )
+ ) {
+ return 'PRI';
+ }
+
+ // SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
+ $data_type = $column_node->get_descendant_node( 'dataType' );
+ if ( null !== $data_type->get_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) {
+ return 'PRI';
+ }
+
+ // 2. UNI: Column has UNIQUE constraint.
+ if ( null !== $column_node->get_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ) ) {
+ return 'UNI';
+ }
+
+ // 3. MUL: Column has INDEX.
+ if ( null !== $column_node->get_descendant_token( WP_MySQL_Lexer::INDEX_SYMBOL ) ) {
+ return 'MUL';
+ }
+
+ return '';
+ }
+
+ private function get_column_extra( WP_Parser_Node $node ): string {
+ $extra = '';
+
+ // SERIAL
+ $data_type = $node->get_descendant_node( 'dataType' );
+ if ( null !== $data_type->get_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) {
+ return 'auto_increment';
+ }
+
+ foreach ( $node->get_descendant_nodes( 'columnAttribute' ) as $attr ) {
+ if ( $attr->has_child_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) ) {
+ return 'auto_increment';
+ }
+ if (
+ $attr->has_child_token( WP_MySQL_Lexer::ON_SYMBOL )
+ && $attr->has_child_token( WP_MySQL_Lexer::UPDATE_SYMBOL )
+ ) {
+ return 'on update CURRENT_TIMESTAMP';
+ }
+ }
+
+ if ( $node->get_descendant_token( WP_MySQL_Lexer::VIRTUAL_SYMBOL ) ) {
+ $extra = 'VIRTUAL GENERATED';
+ } elseif ( $node->get_descendant_token( WP_MySQL_Lexer::STORED_SYMBOL ) ) {
+ $extra = 'STORED GENERATED';
+ }
+ return $extra;
+ }
+
+ private function get_column_comment( WP_Parser_Node $node ): string {
+ foreach ( $node->get_descendant_nodes( 'columnAttribute' ) as $attr ) {
+ if ( $attr->has_child_token( WP_MySQL_Lexer::COMMENT_SYMBOL ) ) {
+ return $this->get_value( $attr->get_child_node( 'textLiteral' ) );
+ }
+ }
+ return '';
+ }
+
+ private function get_column_data_types( WP_Parser_Node $node ): array {
+ $type_node = $node->get_descendant_node( 'dataType' );
+ $type = $type_node->get_descendant_tokens();
+ $token = $type[0];
+
+ // Normalize types.
+ if ( isset( self::TOKEN_TO_TYPE_MAP[ $token->id ] ) ) {
+ $type = self::TOKEN_TO_TYPE_MAP[ $token->id ];
+ } elseif (
+ // VARCHAR/NVARCHAR
+ // NCHAR/NATIONAL VARCHAR
+ // CHAR/CHARACTER/NCHAR VARYING
+ // NATIONAL CHAR/CHARACTER VARYING
+ WP_MySQL_Lexer::VARCHAR_SYMBOL === $token->id
+ || WP_MySQL_Lexer::NVARCHAR_SYMBOL === $token->id
+ || ( isset( $type[1] ) && WP_MySQL_Lexer::VARCHAR_SYMBOL === $type[1]->id )
+ || ( isset( $type[1] ) && WP_MySQL_Lexer::VARYING_SYMBOL === $type[1]->id )
+ || ( isset( $type[2] ) && WP_MySQL_Lexer::VARYING_SYMBOL === $type[2]->id )
+ ) {
+ $type = 'varchar';
+ } elseif (
+ // CHAR, NCHAR, NATIONAL CHAR
+ WP_MySQL_Lexer::CHAR_SYMBOL === $token->id
+ || WP_MySQL_Lexer::NCHAR_SYMBOL === $token->id
+ || isset( $type[1] ) && WP_MySQL_Lexer::CHAR_SYMBOL === $type[1]->id
+ ) {
+ $type = 'char';
+ } elseif (
+ // LONG VARBINARY
+ WP_MySQL_Lexer::LONG_SYMBOL === $token->id
+ && isset( $type[1] ) && WP_MySQL_Lexer::VARBINARY_SYMBOL === $type[1]->id
+ ) {
+ $type = 'mediumblob';
+ } elseif (
+ // LONG CHAR/CHARACTER, LONG CHAR/CHARACTER VARYING
+ WP_MySQL_Lexer::LONG_SYMBOL === $token->id
+ && isset( $type[1] ) && WP_MySQL_Lexer::CHAR_SYMBOL === $type[1]->id
+ ) {
+ $type = 'mediumtext';
+ } elseif (
+ // LONG VARCHAR
+ WP_MySQL_Lexer::LONG_SYMBOL === $token->id
+ && isset( $type[1] ) && WP_MySQL_Lexer::VARCHAR_SYMBOL === $type[1]->id
+ ) {
+ $type = 'mediumtext';
+ } else {
+ throw new \RuntimeException( 'Unknown data type: ' . $token->value );
+ }
+
+ // Get full type.
+ $full_type = $type;
+ if ( 'enum' === $type || 'set' === $type ) {
+ $string_list = $type_node->get_descendant_node( 'stringList' );
+ $values = $string_list->get_child_nodes( 'textString' );
+ foreach ( $values as $i => $value ) {
+ $values[ $i ] = "'" . str_replace( "'", "''", $this->get_value( $value ) ) . "'";
+ }
+ $full_type .= '(' . implode( ',', $values ) . ')';
+ }
+
+ $field_length = $type_node->get_descendant_node( 'fieldLength' );
+ if ( null !== $field_length ) {
+ if ( 'decimal' === $type || 'float' === $type || 'double' === $type ) {
+ $full_type .= rtrim( $this->get_value( $field_length ), ')' ) . ',0)';
+ } else {
+ $full_type .= $this->get_value( $field_length );
+ }
+ /*
+ * As of MySQL 8.0.17, the display width attribute is deprecated for
+ * integer types (tinyint, smallint, mediumint, int/integer, bigint)
+ * and is not stored anymore. However, it may be important for older
+ * versions and WP's dbDelta, so it is safer to keep it at the moment.
+ * @TODO: Investigate if it is important to keep this.
+ */
+ }
+
+ $precision = $type_node->get_descendant_node( 'precision' );
+ if ( null !== $precision ) {
+ $full_type .= $this->get_value( $precision );
+ }
+
+ $datetime_precision = $type_node->get_descendant_node( 'typeDatetimePrecision' );
+ if ( null !== $datetime_precision ) {
+ $full_type .= $this->get_value( $datetime_precision );
+ }
+
+ if (
+ WP_MySQL_Lexer::BOOL_SYMBOL === $token->id
+ || WP_MySQL_Lexer::BOOLEAN_SYMBOL === $token->id
+ ) {
+ $full_type .= '(1)'; // Add length for booleans.
+ }
+
+ if ( null === $field_length && null === $precision ) {
+ if ( 'decimal' === $type ) {
+ $full_type .= '(10,0)'; // Add default precision for decimals.
+ } elseif ( 'char' === $type || 'bit' === $type || 'binary' === $type ) {
+ $full_type .= '(1)'; // Add default length for char, bit, binary.
+ }
+ }
+
+ // UNSIGNED.
+ // SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
+ if (
+ $type_node->get_descendant_token( WP_MySQL_Lexer::UNSIGNED_SYMBOL )
+ || $type_node->get_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL )
+ ) {
+ $full_type .= ' unsigned';
+ }
+
+ // ZEROFILL.
+ if ( $type_node->get_descendant_token( WP_MySQL_Lexer::ZEROFILL_SYMBOL ) ) {
+ $full_type .= ' zerofill';
+ }
+
+ return array( $type, $full_type );
+ }
+
+ private function get_column_charset_and_collation( WP_Parser_Node $node, string $data_type ): array {
+ if ( ! (
+ 'char' === $data_type
+ || 'varchar' === $data_type
+ || 'tinytext' === $data_type
+ || 'text' === $data_type
+ || 'mediumtext' === $data_type
+ || 'longtext' === $data_type
+ || 'enum' === $data_type
+ || 'set' === $data_type
+ ) ) {
+ return array( null, null );
+ }
+
+ $charset = null;
+ $collation = null;
+ $is_binary = false;
+
+ // Charset.
+ $charset_node = $node->get_descendant_node( 'charsetWithOptBinary' );
+ if ( null !== $charset_node ) {
+ $charset_name_node = $charset_node->get_child_node( 'charsetName' );
+ if ( null !== $charset_name_node ) {
+ $charset = strtolower( $this->get_value( $charset_name_node ) );
+ } elseif ( $charset_node->has_child_token( WP_MySQL_Lexer::ASCII_SYMBOL ) ) {
+ $charset = 'latin1';
+ } elseif ( $charset_node->has_child_token( WP_MySQL_Lexer::UNICODE_SYMBOL ) ) {
+ $charset = 'ucs2';
+ } elseif ( $charset_node->has_child_token( WP_MySQL_Lexer::BYTE_SYMBOL ) ) {
+ // @TODO: This changes varchar to varbinary.
+ }
+
+ // @TODO: "DEFAULT"
+
+ if ( $charset_node->has_child_token( WP_MySQL_Lexer::BINARY_SYMBOL ) ) {
+ $is_binary = true;
+ }
+ } else {
+ // National charsets (in MySQL, it's "utf8").
+ $data_type_node = $node->get_descendant_node( 'dataType' );
+ if (
+ $data_type_node->has_child_node( 'nchar' )
+ || $data_type_node->has_child_token( WP_MySQL_Lexer::NCHAR_SYMBOL )
+ || $data_type_node->has_child_token( WP_MySQL_Lexer::NATIONAL_SYMBOL )
+ || $data_type_node->has_child_token( WP_MySQL_Lexer::NVARCHAR_SYMBOL )
+ ) {
+ $charset = 'utf8';
+ }
+ }
+
+ // Normalize charset.
+ if ( 'utf8mb3' === $charset ) {
+ $charset = 'utf8';
+ }
+
+ // Collation.
+ $collation_node = $node->get_descendant_node( 'collationName' );
+ if ( null !== $collation_node ) {
+ $collation = strtolower( $this->get_value( $collation_node ) );
+ }
+
+ // Defaults.
+ // @TODO: These are hardcoded now. We should get them from table/DB.
+ if ( null === $charset && null === $collation ) {
+ $charset = 'utf8mb4';
+ // @TODO: "BINARY" (seems to change varchar to varbinary).
+ // @TODO: "DEFAULT"
+ }
+
+ // If only one of charset/collation is set, the other one is derived.
+ if ( null === $collation ) {
+ if ( $is_binary ) {
+ $collation = $charset . '_bin';
+ } elseif ( isset( self::CHARSET_DEFAULT_COLLATION_MAP[ $charset ] ) ) {
+ $collation = self::CHARSET_DEFAULT_COLLATION_MAP[ $charset ];
+ } else {
+ $collation = $charset . '_general_ci';
+ }
+ } elseif ( null === $charset ) {
+ $charset = substr( $collation, 0, strpos( $collation, '_' ) );
+ }
+
+ return array( $charset, $collation );
+ }
+
+ private function get_column_lengths( WP_Parser_Node $node, string $data_type, ?string $charset ): array {
+ // Text and blob types.
+ if ( 'tinytext' === $data_type || 'tinyblob' === $data_type ) {
+ return array( 255, 255 );
+ } elseif ( 'text' === $data_type || 'blob' === $data_type ) {
+ return array( 65535, 65535 );
+ } elseif ( 'mediumtext' === $data_type || 'mediumblob' === $data_type ) {
+ return array( 16777215, 16777215 );
+ } elseif ( 'longtext' === $data_type || 'longblob' === $data_type ) {
+ return array( 4294967295, 4294967295 );
+ }
+
+ // For CHAR, VARCHAR, BINARY, VARBINARY, we need to check the field length.
+ if (
+ 'char' === $data_type
+ || 'binary' === $data_type
+ || 'varchar' === $data_type
+ || 'varbinary' === $data_type
+ ) {
+ $field_length = $node->get_descendant_node( 'fieldLength' );
+ if ( null === $field_length ) {
+ $length = 1;
+ } else {
+ $length = (int) trim( $this->get_value( $field_length ), '()' );
+ }
+
+ if ( 'char' === $data_type || 'varchar' === $data_type ) {
+ $max_bytes_per_char = self::CHARSET_MAX_BYTES_MAP[ $charset ] ?? 1;
+ return array( $length, $max_bytes_per_char * $length );
+ } else {
+ return array( $length, $length );
+ }
+ }
+
+ // For ENUM and SET, we need to check the longest value.
+ if ( 'enum' === $data_type || 'set' === $data_type ) {
+ $string_list = $node->get_descendant_node( 'stringList' );
+ $values = $string_list->get_child_nodes( 'textString' );
+ $length = 0;
+ foreach ( $values as $value ) {
+ $length = max( $length, strlen( $this->get_value( $value ) ) );
+ }
+ $max_bytes_per_char = self::CHARSET_MAX_BYTES_MAP[ $charset ] ?? 1;
+ return array( $length, $max_bytes_per_char * $length );
+ }
+
+ return array( null, null );
+ }
+
+ private function get_column_numeric_attributes( WP_Parser_Node $node, string $data_type ): array {
+ if ( 'tinyint' === $data_type ) {
+ return array( 3, 0 );
+ } elseif ( 'smallint' === $data_type ) {
+ return array( 5, 0 );
+ } elseif ( 'mediumint' === $data_type ) {
+ return array( 7, 0 );
+ } elseif ( 'int' === $data_type ) {
+ return array( 10, 0 );
+ } elseif ( 'bigint' === $data_type ) {
+ if ( null !== $node->get_descendant_token( WP_MySQL_Lexer::UNSIGNED_SYMBOL ) ) {
+ return array( 20, 0 );
+ }
+
+ // SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
+ $data_type = $node->get_descendant_node( 'dataType' );
+ if ( null !== $data_type->get_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) {
+ return array( 20, 0 );
+ }
+
+ return array( 19, 0 );
+ }
+
+ // For bit columns, we need to check the precision.
+ if ( 'bit' === $data_type ) {
+ $field_length = $node->get_descendant_node( 'fieldLength' );
+ if ( null === $field_length ) {
+ return array( 1, null );
+ }
+ return array( (int) trim( $this->get_value( $field_length ), '()' ), null );
+ }
+
+ // For floating point numbers, we need to check the precision and scale.
+ $precision = null;
+ $scale = null;
+ $precision_node = $node->get_descendant_node( 'precision' );
+ if ( null !== $precision_node ) {
+ $values = $precision_node->get_descendant_tokens( WP_MySQL_Lexer::INT_NUMBER );
+ $precision = (int) $values[0]->value;
+ $scale = (int) $values[1]->value;
+ }
+
+ if ( 'float' === $data_type ) {
+ return array( $precision ?? 12, $scale );
+ } elseif ( 'double' === $data_type ) {
+ return array( $precision ?? 22, $scale );
+ } elseif ( 'decimal' === $data_type ) {
+ if ( null === $precision ) {
+ // Only precision can be specified ("fieldLength" in the grammar).
+ $field_length = $node->get_descendant_node( 'fieldLength' );
+ if ( null !== $field_length ) {
+ $precision = (int) trim( $this->get_value( $field_length ), '()' );
+ }
+ }
+ return array( $precision ?? 10, $scale ?? 0 );
+ }
+
+ return array( null, null );
+ }
+
+ private function get_column_datetime_precision( WP_Parser_Node $node, string $data_type ): ?int {
+ if ( 'time' === $data_type || 'datetime' === $data_type || 'timestamp' === $data_type ) {
+ $precision = $node->get_descendant_node( 'typeDatetimePrecision' );
+ if ( null === $precision ) {
+ return 0;
+ } else {
+ return (int) $this->get_value( $precision );
+ }
+ }
+ return null;
+ }
+
+ private function get_column_generation_expression( WP_Parser_Node $node ): string {
+ if ( null !== $node->get_descendant_token( WP_MySQL_Lexer::GENERATED_SYMBOL ) ) {
+ $expr = $node->get_descendant_node( 'exprWithParentheses' );
+ return $this->get_value( $expr );
+ }
+ return '';
+ }
+
/**
* This is a helper function to get the full unescaped value of a node.
*
From 6bdb7aae56a097276a48ef17b197bc7d7b71aeb2 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 19 Dec 2024 15:22:19 +0100
Subject: [PATCH 018/124] Record CREATE TABLE constraint info in information
schema
---
...s-wp-sqlite-information-schema-builder.php | 259 ++++++++++++++++--
1 file changed, 241 insertions(+), 18 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index 419fa80..037b4c2 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -3,8 +3,10 @@
// @TODO: Remove the namespace and use statements when replacing the old driver.
namespace WIP;
+use PDO;
use PDOStatement;
use WP_MySQL_Lexer;
+use WP_MySQL_Token;
use WP_Parser_Node;
class WP_SQLite_Information_Schema_Builder {
@@ -98,24 +100,24 @@ class WP_SQLite_Information_Schema_Builder {
// STATISTICS (indexes)
"CREATE TABLE IF NOT EXISTS _mysql_information_schema_statistics (
- TABLE_CATALOG TEXT NOT NULL DEFAULT 'def',
- TABLE_SCHEMA TEXT NOT NULL,
- TABLE_NAME TEXT NOT NULL,
- NON_UNIQUE INTEGER NOT NULL,
- INDEX_SCHEMA TEXT NOT NULL,
- INDEX_NAME TEXT NOT NULL,
- SEQ_IN_INDEX INTEGER NOT NULL,
- COLUMN_NAME TEXT,
- COLLATION TEXT,
- CARDINALITY INTEGER,
- SUB_PART INTEGER,
- PACKED TEXT,
- NULLABLE TEXT NOT NULL,
- INDEX_TYPE TEXT NOT NULL,
- COMMENT TEXT NOT NULL DEFAULT '',
- INDEX_COMMENT TEXT NOT NULL DEFAULT '',
- IS_VISIBLE TEXT NOT NULL DEFAULT 'YES',
- EXPRESSION TEXT
+ TABLE_CATALOG TEXT NOT NULL DEFAULT 'def', -- always 'def'
+ TABLE_SCHEMA TEXT NOT NULL, -- database name
+ TABLE_NAME TEXT NOT NULL, -- table name
+ NON_UNIQUE INTEGER NOT NULL, -- 0 for unique indexes, 1 otherwise
+ INDEX_SCHEMA TEXT NOT NULL, -- index database name
+ INDEX_NAME TEXT NOT NULL, -- index name, for PKs always 'PRIMARY'
+ SEQ_IN_INDEX INTEGER NOT NULL, -- column position in index (from 1)
+ COLUMN_NAME TEXT, -- column name (NULL for functional indexes)
+ COLLATION TEXT, -- column sort in the index ('A', 'D', or NULL)
+ CARDINALITY INTEGER, -- not implemented
+ SUB_PART INTEGER, -- number of indexed chars, NULL for full column
+ PACKED TEXT, -- not implemented
+ NULLABLE TEXT NOT NULL, -- 'YES' if column can contain NULL, '' otherwise
+ INDEX_TYPE TEXT NOT NULL, -- 'BTREE', 'FULLTEXT', 'SPATIAL'
+ COMMENT TEXT NOT NULL DEFAULT '', -- not implemented
+ INDEX_COMMENT TEXT NOT NULL DEFAULT '', -- index comment
+ IS_VISIBLE TEXT NOT NULL DEFAULT 'YES', -- 'NO' if column is hidden, 'YES' otherwise
+ EXPRESSION TEXT -- expression for functional indexes
) STRICT",
// TABLE_CONSTRAINTS
@@ -399,6 +401,106 @@ public function record_create_table( WP_Parser_Node $node ): void {
$this->insert_values( '_mysql_information_schema_columns', $column_data );
$column_position += 1;
}
+
+ // 3. Constraints.
+ foreach ( $node->get_descendant_nodes( 'tableConstraintDef' ) as $constraint_node ) {
+ $this->record_add_constraint( $table_name, $constraint_node );
+ }
+ }
+
+ private function record_add_constraint( string $table_name, WP_Parser_Node $node ): void {
+ // Get first constraint keyword.
+ $children = $node->get_children();
+ $keyword = $children[0] instanceof WP_MySQL_Token ? $children[0] : $children[1];
+ if ( ! $keyword instanceof WP_MySQL_Token ) {
+ $keyword = $keyword->get_child_token();
+ }
+
+ if (
+ WP_MySQL_Lexer::FOREIGN_SYMBOL === $keyword->id
+ || WP_MySQL_Lexer::CHECK_SYMBOL === $keyword->id
+ ) {
+ throw new \Exception( 'FOREIGN KEY and CHECK constraints are not supported yet.' );
+ }
+
+ // Fetch column info.
+ $column_info = $this->query(
+ '
+ SELECT column_name, data_type, is_nullable, character_maximum_length
+ FROM _mysql_information_schema_columns
+ WHERE table_name = ?
+ ',
+ array( $table_name )
+ )->fetchAll( PDO::FETCH_ASSOC );
+
+ $column_info_map = array_combine(
+ array_column( $column_info, 'COLUMN_NAME' ),
+ $column_info
+ );
+
+ // Get first index column data type (needed for index type).
+ $first_column_name = $column_info[0]['COLUMN_NAME'];
+ $first_column_type = $column_info_map[ $first_column_name ]['DATA_TYPE'] ?? null;
+ $has_spatial_column = null !== $first_column_type && $this->is_spatial_data_type( $first_column_type );
+
+ $non_unique = $this->get_index_non_unique( $keyword );
+ $index_name = $this->get_index_name( $node );
+ $index_type = $this->get_index_type( $node, $keyword, $has_spatial_column );
+
+ $key_list = $node->get_child_node( 'keyListVariants' )->get_child();
+ if ( 'keyListWithExpression' === $key_list->rule_name ) {
+ $key_parts = array();
+ foreach ( $key_list->get_descendant_nodes( 'keyPartOrExpression' ) as $key_part ) {
+ $key_parts[] = $key_part->get_child();
+ }
+ } else {
+ $key_parts = $key_list->get_descendant_nodes( 'keyPart' );
+ }
+
+ $seq_in_index = 1;
+ foreach ( $key_parts as $key_part ) {
+ $column_name = $this->get_index_column_name( $key_part );
+ $collation = $this->get_index_column_collation( $key_part, $index_type );
+ if (
+ 'PRIMARY' === $index_name
+ || 'NO' === $column_info_map[ $column_name ]['IS_NULLABLE']
+ ) {
+ $nullable = '';
+ } else {
+ $nullable = 'YES';
+ }
+
+ $sub_part = $this->get_index_column_sub_part(
+ $key_part,
+ $column_info_map[ $column_name ]['CHARACTER_MAXIMUM_LENGTH'],
+ $has_spatial_column
+ );
+
+ $this->insert_values(
+ '_mysql_information_schema_statistics',
+ array(
+ 'table_schema' => $this->db_name,
+ 'table_name' => $table_name,
+ 'non_unique' => $non_unique,
+ 'index_schema' => $this->db_name,
+ 'index_name' => $index_name,
+ 'seq_in_index' => $seq_in_index,
+ 'column_name' => $column_name,
+ 'collation' => $collation,
+ 'cardinality' => 0, // not implemented
+ 'sub_part' => $sub_part,
+ 'packed' => null, // not implemented
+ 'nullable' => $nullable,
+ 'index_type' => $index_type,
+ 'comment' => '', // not implemented
+ 'index_comment' => '', // @TODO
+ 'is_visible' => 'YES', // @TODO: Save actual visibility value.
+ 'expression' => null, // @TODO
+ )
+ );
+
+ $seq_in_index += 1;
+ }
}
private function extract_column_data( string $table_name, string $column_name, WP_Parser_Node $node, int $position ): array {
@@ -896,6 +998,127 @@ private function get_column_generation_expression( WP_Parser_Node $node ): strin
return '';
}
+ private function get_index_name( WP_Parser_Node $node ): string {
+ if ( $node->get_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) {
+ return 'PRIMARY';
+ }
+
+ $name_node = $node->get_descendant_node( 'indexName' );
+ if ( null === $name_node ) {
+ /*
+ * In MySQL, the default index name equals the first column name.
+ * For functional indexes, the string "functional_index" is used.
+ * If the name is already used, we need to append a number.
+ */
+ $subnode = $node->get_child_node( 'keyListVariants' )->get_child_node();
+ if ( 'exprWithParentheses' === $subnode->rule_name ) {
+ $name = 'functional_index';
+ } else {
+ $name = $this->get_value( $subnode->get_descendant_node( 'identifier' ) );
+ }
+
+ // @TODO: Check if the name is already used.
+ return $name;
+ }
+ return $this->get_value( $name_node );
+ }
+
+ private function get_index_non_unique( WP_MySQL_Token $token ): int {
+ if (
+ WP_MySQL_Lexer::PRIMARY_SYMBOL === $token->id
+ || WP_MySQL_Lexer::UNIQUE_SYMBOL === $token->id
+ ) {
+ return 0;
+ }
+ return 1;
+ }
+
+ private function get_index_type(
+ WP_Parser_Node $node,
+ WP_MySQL_Token $token,
+ bool $has_spatial_column
+ ): string {
+ // Handle "USING ..." clause.
+ $index_type = $node->get_descendant_node( 'indexTypeClause' );
+ if ( null !== $index_type ) {
+ $index_type = strtoupper(
+ $this->get_value( $index_type->get_child_node( 'indexType' ) )
+ );
+ if ( 'RTREE' === $index_type ) {
+ return 'SPATIAL';
+ } elseif ( 'HASH' === $index_type ) {
+ // InnoDB uses BTREE even when HASH is specified.
+ return 'BTREE';
+ }
+ return $index_type;
+ }
+
+ // Derive index type from its definition.
+ if ( WP_MySQL_Lexer::FULLTEXT_SYMBOL === $token->id ) {
+ return 'FULLTEXT';
+ } elseif ( WP_MySQL_Lexer::SPATIAL_SYMBOL === $token->id ) {
+ return 'SPATIAL';
+ }
+
+ // Spatial indexes are also derived from column data type.
+ if ( $has_spatial_column ) {
+ return 'SPATIAL';
+ }
+
+ return 'BTREE';
+ }
+
+ private function get_index_column_name( WP_Parser_Node $node ): ?string {
+ if ( 'keyPart' !== $node->rule_name ) {
+ return null;
+ }
+ return $this->get_value( $node->get_descendant_node( 'identifier' ) );
+ }
+
+ private function get_index_column_collation( WP_Parser_Node $node, string $index_type ): ?string {
+ if ( 'FULLTEXT' === $index_type ) {
+ return null;
+ }
+
+ $collate_node = $node->get_descendant_node( 'collationName' );
+ if ( null === $collate_node ) {
+ return 'A';
+ }
+ $collate = strtoupper( $this->get_value( $collate_node ) );
+ return 'DESC' === $collate ? 'D' : 'A';
+ }
+
+ private function get_index_column_sub_part(
+ WP_Parser_Node $node,
+ ?int $max_length,
+ bool $is_spatial
+ ): ?int {
+ $field_length = $node->get_descendant_node( 'fieldLength' );
+ if ( null === $field_length ) {
+ if ( $is_spatial ) {
+ return 32;
+ }
+ return null;
+ }
+
+ $value = (int) trim( $this->get_value( $field_length ), '()' );
+ if ( null !== $max_length && $value >= $max_length ) {
+ return $max_length;
+ }
+ return $value;
+ }
+
+ private function is_spatial_data_type( string $data_type ): bool {
+ return 'geometry' === $data_type
+ || 'geomcollection' === $data_type
+ || 'point' === $data_type
+ || 'multipoint' === $data_type
+ || 'linestring' === $data_type
+ || 'multilinestring' === $data_type
+ || 'polygon' === $data_type
+ || 'multipolygon' === $data_type;
+ }
+
/**
* This is a helper function to get the full unescaped value of a node.
*
From afeeb18787edc820d101842627c8cbb0f9ecb82e Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 19 Dec 2024 15:23:32 +0100
Subject: [PATCH 019/124] Record CREATE TABLE inline constraint info in
information schema
---
...s-wp-sqlite-information-schema-builder.php | 44 +++++++++++++++++++
1 file changed, 44 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index 037b4c2..e685eb3 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -399,6 +399,21 @@ public function record_create_table( WP_Parser_Node $node ): void {
$column_position
);
$this->insert_values( '_mysql_information_schema_columns', $column_data );
+
+ // Inline column constraint.
+ $column_constraint_data = $this->extract_column_constraint_data(
+ $table_name,
+ $column_name,
+ $column_node,
+ 'YES' === $column_data['is_nullable']
+ );
+ if ( null !== $column_constraint_data ) {
+ $this->insert_values(
+ '_mysql_information_schema_statistics',
+ $column_constraint_data
+ );
+ }
+
$column_position += 1;
}
@@ -542,6 +557,35 @@ private function extract_column_data( string $table_name, string $column_name, W
);
}
+ private function extract_column_constraint_data( string $table_name, string $column_name, WP_Parser_Node $node, bool $nullable ): ?array {
+ // Handle inline PRIMARY KEY and UNIQUE constraints.
+ $has_inline_primary_key = null !== $node->get_descendant_token( WP_MySQL_Lexer::KEY_SYMBOL );
+ $has_inline_unique_key = null !== $node->get_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL );
+ if ( $has_inline_primary_key || $has_inline_unique_key ) {
+ $index_name = $has_inline_primary_key ? 'PRIMARY' : $column_name;
+ return array(
+ 'table_schema' => $this->db_name,
+ 'table_name' => $table_name,
+ 'non_unique' => 0,
+ 'index_schema' => $this->db_name,
+ 'index_name' => $index_name,
+ 'seq_in_index' => 1,
+ 'column_name' => $column_name,
+ 'collation' => 'A',
+ 'cardinality' => 0, // not implemented
+ 'sub_part' => null,
+ 'packed' => null, // not implemented
+ 'nullable' => true === $nullable ? 'YES' : '',
+ 'index_type' => 'BTREE',
+ 'comment' => '', // not implemented
+ 'index_comment' => '', // @TODO
+ 'is_visible' => 'YES', // @TODO: Save actual visibility value.
+ 'expression' => null, // @TODO
+ );
+ }
+ return null;
+ }
+
private function get_table_engine( WP_Parser_Node $node ): string {
$engine_node = $node->get_descendant_node( 'engineRef' );
if ( null === $engine_node ) {
From 2eaf0774fea1fc8cc44b8a8ca4ff409a437e6164 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 19 Dec 2024 15:28:36 +0100
Subject: [PATCH 020/124] Sync constraint info to columns table when
constraints are modified
---
...s-wp-sqlite-information-schema-builder.php | 47 +++++++++++++++++++
1 file changed, 47 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index e685eb3..1561801 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -516,6 +516,8 @@ private function record_add_constraint( string $table_name, WP_Parser_Node $node
$seq_in_index += 1;
}
+
+ $this->sync_column_key_info( $table_name );
}
private function extract_column_data( string $table_name, string $column_name, WP_Parser_Node $node, int $position ): array {
@@ -586,6 +588,51 @@ private function extract_column_constraint_data( string $table_name, string $col
return null;
}
+ /**
+ * Update column info from constraint data in the statistics table.
+ *
+ * When constraints are added or removed, we need to reflect the changes
+ * in the "COLUMN_KEY" and "IS_NULLABLE" columns of the "COLUMNS" table.
+ *
+ * A) COLUMN_KEY (priority from 1 to 4):
+ * 1. "PRI": Column is any component of a PRIMARY KEY.
+ * 2. "UNI": Column is the first column of a UNIQUE KEY.
+ * 3. "MUL": Column is the first column of a non-unique index.
+ * 4. "": Column is not indexed.
+ *
+ * B) IS_NULLABLE: In COLUMNS, "YES"/"NO". In STATISTICS, "YES"/"".
+ */
+ private function sync_column_key_info( string $table_name ): void {
+ // @TODO: Consider listing only affected columns.
+ $this->query(
+ "
+ WITH s AS (
+ SELECT
+ column_name,
+ CASE
+ WHEN MAX(index_name = 'PRIMARY') THEN 'PRI'
+ WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI'
+ WHEN MAX(seq_in_index = 1) THEN 'MUL'
+ ELSE ''
+ END AS column_key
+ FROM _mysql_information_schema_statistics
+ WHERE table_schema = ?
+ AND table_name = ?
+ GROUP BY column_name
+ )
+ UPDATE _mysql_information_schema_columns AS c
+ SET
+ column_key = s.column_key,
+ is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable)
+ FROM s
+ WHERE c.table_schema = ?
+ AND c.table_name = ?
+ AND s.column_name = c.column_name
+ ",
+ array( $this->db_name, $table_name, $this->db_name, $table_name )
+ );
+ }
+
private function get_table_engine( WP_Parser_Node $node ): string {
$engine_node = $node->get_descendant_node( 'engineRef' );
if ( null === $engine_node ) {
From d03892d77731fd003a4e94d093dfdd8603adda5b Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 19 Dec 2024 21:02:38 +0100
Subject: [PATCH 021/124] Record ALTER TABLE ADD COLUMN(s) in information
schema
---
...s-wp-sqlite-information-schema-builder.php | 69 +++++++++++++++++++
1 file changed, 69 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index 1561801..4d5ab5a 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -423,6 +423,54 @@ public function record_create_table( WP_Parser_Node $node ): void {
}
}
+ public function record_alter_table( WP_Parser_Node $node ): void {
+ $table_name = $this->get_value( $node->get_descendant_node( 'tableRef' ) );
+ $actions = $node->get_descendant_nodes( 'alterListItem' );
+
+ foreach ( $actions as $action ) {
+ $first_token = $action->get_child_token();
+
+ // ADD
+ if ( WP_MySQL_Lexer::ADD_SYMBOL === $first_token->id ) {
+ // ADD [COLUMN] (...[, ...])
+ $column_definitions = $action->get_descendant_nodes( 'columnDefinition' );
+ if ( count( $column_definitions ) > 0 ) {
+ foreach ( $column_definitions as $column_definition ) {
+ $name = $this->get_value( $column_definition->get_child_node( 'identifier' ) );
+ $this->record_add_column( $table_name, $name, $column_definition );
+ }
+ continue;
+ }
+
+ // ADD [COLUMN] ...
+ $field_definition = $action->get_descendant_node( 'fieldDefinition' );
+ if ( null !== $field_definition ) {
+ $name = $this->get_value( $action->get_child_node( 'identifier' ) );
+ $this->record_add_column( $table_name, $name, $field_definition );
+ // @TODO: Handle FIRST/AFTER.
+ continue;
+ }
+
+ throw new \Exception( sprintf( 'Unsupported ALTER TABLE ADD action: %s', $first_token->value ) );
+ }
+ }
+ }
+
+ private function record_add_column( string $table_name, string $column_name, WP_Parser_Node $node ): void {
+ $position = $this->query(
+ 'SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = ?',
+ array( $table_name )
+ )->fetchColumn();
+
+ $column_data = $this->extract_column_data( $table_name, $column_name, $node, (int) $position + 1 );
+ $this->insert_values( '_mysql_information_schema_columns', $column_data );
+
+ $column_constraint_data = $this->extract_column_constraint_data( $table_name, $column_name, $node, true );
+ if ( null !== $column_constraint_data ) {
+ $this->insert_values( '_mysql_information_schema_statistics', $column_constraint_data );
+ }
+ }
+
private function record_add_constraint( string $table_name, WP_Parser_Node $node ): void {
// Get first constraint keyword.
$children = $node->get_children();
@@ -1264,6 +1312,27 @@ private function insert_values( string $table_name, array $data ): void {
);
}
+ private function update_values( string $table_name, array $data, array $where ): void {
+ $set = array();
+ foreach ( $data as $column => $value ) {
+ $set[] = $column . ' = ?';
+ }
+
+ $where_clause = array();
+ foreach ( $where as $column => $value ) {
+ $where_clause[] = $column . ' = ?';
+ }
+
+ $this->query(
+ '
+ UPDATE ' . $table_name . '
+ SET ' . implode( ', ', $set ) . '
+ WHERE ' . implode( ' AND ', $where_clause ) . '
+ ',
+ array_merge( array_values( $data ), array_values( $where ) )
+ );
+ }
+
/**
* @param string $query
* @param array $params
From 93f536e023e35a38e800d5238417f5eda8375f01 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 19 Dec 2024 21:04:12 +0100
Subject: [PATCH 022/124] Record ALTER TABLE ADD CONSTRAINT in information
schema
---
.../class-wp-sqlite-information-schema-builder.php | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index 4d5ab5a..d6702d0 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -451,6 +451,13 @@ public function record_alter_table( WP_Parser_Node $node ): void {
continue;
}
+ // ADD CONSTRAINT.
+ $constraint = $action->get_descendant_node( 'tableConstraintDef' );
+ if ( null !== $constraint ) {
+ $this->record_add_constraint( $table_name, $constraint );
+ continue;
+ }
+
throw new \Exception( sprintf( 'Unsupported ALTER TABLE ADD action: %s', $first_token->value ) );
}
}
From 6940a5b5c946249ba244dc29d6ef5ae7dca462ff Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 19 Dec 2024 21:04:53 +0100
Subject: [PATCH 023/124] Record ALTER TABLE CHANGE/MODIFY COLUMN in
information schema
---
...s-wp-sqlite-information-schema-builder.php | 76 +++++++++++++++++++
1 file changed, 76 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index d6702d0..3baf5b0 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -460,6 +460,30 @@ public function record_alter_table( WP_Parser_Node $node ): void {
throw new \Exception( sprintf( 'Unsupported ALTER TABLE ADD action: %s', $first_token->value ) );
}
+
+ // CHANGE [COLUMN]
+ if ( WP_MySQL_Lexer::CHANGE_SYMBOL === $first_token->id ) {
+ $old_name = $this->get_value( $action->get_child_node( 'columnInternalRef' ) );
+ $new_name = $this->get_value( $action->get_child_node( 'identifier' ) );
+ $this->record_change_column(
+ $table_name,
+ $old_name,
+ $new_name,
+ $action->get_descendant_node( 'fieldDefinition' )
+ );
+ continue;
+ }
+
+ // MODIFY [COLUMN]
+ if ( WP_MySQL_Lexer::MODIFY_SYMBOL === $first_token->id ) {
+ $name = $this->get_value( $action->get_child_node( 'columnInternalRef' ) );
+ $this->record_modify_column(
+ $table_name,
+ $name,
+ $action->get_descendant_node( 'fieldDefinition' )
+ );
+ continue;
+ }
}
}
@@ -478,6 +502,58 @@ private function record_add_column( string $table_name, string $column_name, WP_
}
}
+ private function record_change_column(
+ string $table_name,
+ string $column_name,
+ string $new_column_name,
+ WP_Parser_Node $node
+ ): void {
+ $column_data = $this->extract_column_data( $table_name, $new_column_name, $node, 0 );
+ $this->update_values(
+ '_mysql_information_schema_columns',
+ $column_data,
+ array(
+ 'table_name' => $table_name,
+ 'column_name' => $column_name,
+ )
+ );
+
+ // Update column name in statistics, if it has changed.
+ if ( $new_column_name !== $column_name ) {
+ $this->update_values(
+ '_mysql_information_schema_statistics',
+ array(
+ 'column_name' => $new_column_name,
+ ),
+ array(
+ 'table_name' => $table_name,
+ 'column_name' => $column_name,
+ )
+ );
+ }
+
+ // Handle inline constraints. When inline constraint is defined, MySQL
+ // always adds a new constraint rather than replacing an existing one.
+ $column_constraint_data = $this->extract_column_constraint_data(
+ $table_name,
+ $new_column_name,
+ $node,
+ 'YES' === $column_data['is_nullable']
+ );
+ if ( null !== $column_constraint_data ) {
+ $this->insert_values( '_mysql_information_schema_statistics', $column_constraint_data );
+ $this->sync_column_key_info( $table_name );
+ }
+ }
+
+ private function record_modify_column(
+ string $table_name,
+ string $column_name,
+ WP_Parser_Node $node
+ ): void {
+ $this->record_change_column( $table_name, $column_name, $column_name, $node );
+ }
+
private function record_add_constraint( string $table_name, WP_Parser_Node $node ): void {
// Get first constraint keyword.
$children = $node->get_children();
From 127efc25b27decef936cb9b523409f60d44e4d64 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 19 Dec 2024 21:05:04 +0100
Subject: [PATCH 024/124] Record ALTER TABLE DROP COLUMN in information schema
---
...s-wp-sqlite-information-schema-builder.php | 60 +++++++++++++++++++
1 file changed, 60 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index 3baf5b0..f5ce252 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -484,6 +484,16 @@ public function record_alter_table( WP_Parser_Node $node ): void {
);
continue;
}
+
+ // DROP
+ if ( WP_MySQL_Lexer::DROP_SYMBOL === $first_token->id ) {
+ // DROP [COLUMN]
+ $column_ref = $action->get_child_node( 'columnInternalRef' );
+ if ( null !== $column_ref ) {
+ $name = $this->get_value( $column_ref );
+ $this->record_drop_column( $table_name, $name );
+ }
+ }
}
}
@@ -554,6 +564,41 @@ private function record_modify_column(
$this->record_change_column( $table_name, $column_name, $column_name, $node );
}
+ private function record_drop_column( $table_name, $column_name ): void {
+ $this->delete_values(
+ '_mysql_information_schema_columns',
+ array(
+ 'table_name' => $table_name,
+ 'column_name' => $column_name,
+ )
+ );
+
+ /**
+ * From MySQL documentation:
+ *
+ * If columns are dropped from a table, the columns are also removed
+ * from any index of which they are a part. If all columns that make up
+ * an index are dropped, the index is dropped as well.
+ *
+ * This means we need to remove the records from the STATISTICS table,
+ * renumber the SEQ_IN_INDEX values, and resync the column key info.
+ *
+ * See:
+ * - https://dev.mysql.com/doc/refman/8.4/en/alter-table.html
+ */
+ $this->delete_values(
+ '_mysql_information_schema_statistics',
+ array(
+ 'table_name' => $table_name,
+ 'column_name' => $column_name,
+ )
+ );
+
+ // @TODO: Renumber SEQ_IN_INDEX values.
+
+ $this->sync_column_key_info( $table_name );
+ }
+
private function record_add_constraint( string $table_name, WP_Parser_Node $node ): void {
// Get first constraint keyword.
$children = $node->get_children();
@@ -1416,6 +1461,21 @@ private function update_values( string $table_name, array $data, array $where ):
);
}
+ private function delete_values( string $table_name, array $where ): void {
+ $where_clause = array();
+ foreach ( $where as $column => $value ) {
+ $where_clause[] = $column . ' = ?';
+ }
+
+ $this->query(
+ '
+ DELETE FROM ' . $table_name . '
+ WHERE ' . implode( ' AND ', $where_clause ) . '
+ ',
+ array_values( $where )
+ );
+ }
+
/**
* @param string $query
* @param array $params
From 71265edbd8ea24a575d0149f10578de1124602f2 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 20 Dec 2024 11:03:23 +0100
Subject: [PATCH 025/124] Record ALTER TABLE DROP INDEX in information schema
---
...s-wp-sqlite-information-schema-builder.php | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index f5ce252..ca0302a 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -492,6 +492,14 @@ public function record_alter_table( WP_Parser_Node $node ): void {
if ( null !== $column_ref ) {
$name = $this->get_value( $column_ref );
$this->record_drop_column( $table_name, $name );
+ continue;
+ }
+
+ // DROP INDEX
+ if ( $action->has_child_node( 'keyOrIndex' ) ) {
+ $name = $this->get_value( $action->get_child_node( 'indexRef' ) );
+ $this->record_drop_index( $table_name, $name );
+ continue;
}
}
}
@@ -599,6 +607,17 @@ private function record_drop_column( $table_name, $column_name ): void {
$this->sync_column_key_info( $table_name );
}
+ private function record_drop_index( string $table_name, string $index_name ): void {
+ $this->delete_values(
+ '_mysql_information_schema_statistics',
+ array(
+ 'table_name' => $table_name,
+ 'index_name' => $index_name,
+ )
+ );
+ $this->sync_column_key_info( $table_name );
+ }
+
private function record_add_constraint( string $table_name, WP_Parser_Node $node ): void {
// Get first constraint keyword.
$children = $node->get_children();
From 71e8d08c5d7f4476b65c9891af52e3ad53048b66 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 20 Dec 2024 12:18:38 +0100
Subject: [PATCH 026/124] Execute CREATE TABLE using information schema
---
.../sqlite-ast/class-wp-sqlite-driver.php | 362 +++++++++++-------
1 file changed, 234 insertions(+), 128 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 7bd37d0..f1a63ee 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -21,6 +21,11 @@ class WP_SQLite_Driver {
const SQLITE_BUSY = 5;
const SQLITE_LOCKED = 6;
+ /**
+ * A map of MySQL tokens to SQLite data types.
+ *
+ * This is used to translate a MySQL data type to an SQLite data type.
+ */
const DATA_TYPE_MAP = array(
// Numeric data types:
WP_MySQL_Lexer::BIT_SYMBOL => 'INTEGER',
@@ -80,6 +85,72 @@ class WP_SQLite_Driver {
// SERIAL, SET, and JSON types are handled in the translation process.
);
+ /**
+ * A map of normalized MySQL data types to SQLite data types.
+ *
+ * This is used to generate SQLite CREATE TABLE statements from the MySQL
+ * INFORMATION_SCHEMA tables. They keys are MySQL data types normalized
+ * as they appear in the INFORMATION_SCHEMA. Values are SQLite data types.
+ */
+ const DATA_TYPE_STRING_MAP = array(
+ // Numeric data types:
+ 'bit' => 'INTEGER',
+ 'bool' => 'INTEGER',
+ 'boolean' => 'INTEGER',
+ 'tinyint' => 'INTEGER',
+ 'smallint' => 'INTEGER',
+ 'mediumint' => 'INTEGER',
+ 'int' => 'INTEGER',
+ 'integer' => 'INTEGER',
+ 'bigint' => 'INTEGER',
+ 'float' => 'REAL',
+ 'double' => 'REAL',
+ 'real' => 'REAL',
+ 'decimal' => 'REAL',
+ 'dec' => 'REAL',
+ 'fixed' => 'REAL',
+ 'numeric' => 'REAL',
+
+ // String data types:
+ 'char' => 'TEXT',
+ 'varchar' => 'TEXT',
+ 'nchar' => 'TEXT',
+ 'nvarchar' => 'TEXT',
+ 'tinytext' => 'TEXT',
+ 'text' => 'TEXT',
+ 'mediumtext' => 'TEXT',
+ 'longtext' => 'TEXT',
+ 'enum' => 'TEXT',
+ 'set' => 'TEXT',
+ 'json' => 'TEXT',
+
+ // Date and time data types:
+ 'date' => 'TEXT',
+ 'time' => 'TEXT',
+ 'datetime' => 'TEXT',
+ 'timestamp' => 'TEXT',
+ 'year' => 'TEXT',
+
+ // Binary data types:
+ 'binary' => 'INTEGER',
+ 'varbinary' => 'BLOB',
+ 'tinyblob' => 'BLOB',
+ 'blob' => 'BLOB',
+ 'mediumblob' => 'BLOB',
+ 'longblob' => 'BLOB',
+
+ // Spatial data types:
+ 'geometry' => 'TEXT',
+ 'point' => 'TEXT',
+ 'linestring' => 'TEXT',
+ 'polygon' => 'TEXT',
+ 'multipoint' => 'TEXT',
+ 'multilinestring' => 'TEXT',
+ 'multipolygon' => 'TEXT',
+ 'geomcollection' => 'TEXT',
+ 'geometrycollection' => 'TEXT',
+ );
+
const DATA_TYPES_CACHE_TABLE = '_mysql_data_types_cache';
const CREATE_DATA_TYPES_CACHE_TABLE = 'CREATE TABLE IF NOT EXISTS _mysql_data_types_cache (
@@ -902,86 +973,25 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void {
return;
}
- /*
- * We need to handle some differences between MySQL and SQLite:
- *
- * 1. Inline index definitions:
- *
- * In MySQL, we can define an index inline with a column definition.
- * In SQLite, we need to define indexes separately, using extra queries.
- *
- * 2. Column and constraint definition order:
- *
- * In MySQL, column and constraint definitions can be arbitrarily mixed.
- * In SQLite, column definitions must come first, followed by constraints.
- *
- * 2. Auto-increment:
- *
- * In MySQL, there can at most one AUTO_INCREMENT column, and it must be
- * a PRIMARY KEY, or the first column in a multi-column KEY.
- *
- * In SQLite, there can at most one AUTOINCREMENT column, and it must be
- * a PRIMARY KEY, defined inline on a single column.
- *
- * Therefore, the following valid MySQL construct is not supported:
- * CREATE TABLE t ( a INT AUTO_INCREMENT, b INT, PRIMARY KEY (a, b) );
- * @TODO: Support it with a single-column PK and a multi-column UNIQUE KEY.
- */
-
- // Collect column, index, and constraint nodes.
- $columns = array();
- $constraints = array();
- $indexes = array();
- $has_autoincrement = false;
- $primary_key_constraint = null; // Does not include inline PK definition.
+ $table_name = $this->unquote_sqlite_identifier(
+ $this->translate( $node->get_descendant_node( 'tableName' ) )
+ );
- foreach ( $element_list->get_descendant_nodes( 'columnDefinition' ) as $child ) {
- if ( null !== $child->get_descendant_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) ) {
- $has_autoincrement = true;
- }
- // @TODO: Collect inline index definitions.
- $columns[] = $child;
- }
+ // Save information to information schema tables.
+ $this->information_schema_builder->record_create_table( $node );
- foreach ( $element_list->get_descendant_nodes( 'tableConstraintDef' ) as $child ) {
- if ( null !== $child->get_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) {
- $primary_key_constraint = $child;
- } else {
- $constraints[] = $child;
- }
- }
+ // Generate CREATE TABLE statement from the information schema tables.
+ $queries = $this->get_sqlite_create_table_statement( $table_name );
+ $create_table_query = $queries[0];
+ $constraint_queries = array_slice( $queries, 1 );
- /*
- * If we have a PRIMARY KEY constraint:
- * 1. Without auto-increment, we can put it back to the list of constraints.
- * 2. With auto-increment, we need to later move it to the column definition.
- */
- if ( null !== $primary_key_constraint ) {
- if ( ! $has_autoincrement ) {
- $constraints[] = $primary_key_constraint;
- } elseif ( count( $primary_key_constraint->get_descendant_nodes( 'keyPart' ) ) > 1 ) {
- throw $this->not_supported_exception(
- 'Composite primary key with AUTO_INCREMENT'
- );
- }
- }
+ $this->execute_sqlite_query( $create_table_query );
+ $this->results = $this->last_exec_returned;
+ $this->return_value = $this->results;
- $query_parts = array( 'CREATE' );
- foreach ( $node->get_child_node()->get_children() as $child ) {
- if ( $child instanceof WP_Parser_Node && 'tableElementList' === $child->rule_name ) {
- $query_parts[] = $this->translate_sequence( array_merge( $columns, $constraints ), ' , ' );
- } else {
- $part = $this->translate( $child );
- if ( null !== $part ) {
- $query_parts[] = $part;
- }
- }
+ foreach ( $constraint_queries as $query ) {
+ $this->execute_sqlite_query( $query );
}
-
- // @TODO: Execute queries for inline index definitions.
-
- $this->execute_sqlite_query( implode( ' ', $query_parts ) );
- $this->set_result_from_affected_rows();
}
private function execute_alter_table_statement( WP_Parser_Node $node ): void {
@@ -1116,59 +1126,9 @@ private function translate( $ast ) {
// When we have no value, it's reasonable to use NULL.
return 'NULL';
- case 'fieldDefinition':
- /*
- * In SQLite, there is the a quirk for backward compatibility:
- * 1. INTEGER PRIMARY KEY creates an alias of ROWID.
- * 2. INT PRIMARY KEY will not alias of ROWID.
- *
- * Therefore, we want to:
- * 1. Use INTEGER PRIMARY KEY for when we have AUTOINCREMENT.
- * 2. Use INT PRIMARY KEY otherwise.
- */
- $has_primary_key = $ast->get_descendant_token( WP_MySQL_Lexer::KEY_SYMBOL ) !== null;
- $has_autoincrement = $ast->get_descendant_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) !== null;
- $children = $ast->get_children();
- $data_type_node = array_shift( $children );
- $data_type = $this->translate( $data_type_node );
- if ( $has_primary_key && 'INTEGER' === $data_type ) {
- $data_type = $has_autoincrement ? 'INTEGER' : 'INT';
- }
-
- $attributes = $this->translate_sequence( $children );
- $definition = $data_type . ( null === $attributes ? '' : " $attributes" );
-
- /*
- * In SQLite, AUTOINCREMENT must always be preceded by PRIMARY KEY.
- * Therefore, we remove both PRIMARY KEY and AUTOINCREMENT from
- * column attributes, and append them here in SQLite-friendly way.
- */
- if ( $has_autoincrement ) {
- return $definition . ' PRIMARY KEY AUTOINCREMENT';
- } elseif ( $has_primary_key ) {
- return $definition . ' PRIMARY KEY';
- }
- return $definition;
- case 'columnAttribute':
- case 'gcolAttribute':
- /*
- * Remove PRIMARY KEY and AUTOINCREMENT from the column attributes.
- * They are handled in the "fieldDefinition" node.
- */
- if ( $ast->has_child_token( WP_MySQL_Lexer::KEY_SYMBOL ) ) {
- return null;
- }
- if ( $ast->has_child_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) ) {
- return null;
- }
- return $this->translate_sequence( $ast->get_children() );
- case 'createTableOptions':
- return $this->translate_sequence( $ast->get_children(), ', ' );
- case 'createTableOption':
- if ( $ast->get_child_token( WP_MySQL_Lexer::ENGINE_SYMBOL ) ) {
- return null;
- }
- return $this->translate_sequence( $ast->get_children() );
+ case 'defaultCollation':
+ // @TODO: Check and save in information schema.
+ return null;
case 'duplicateAsQueryExpression':
// @TODO: How to handle IGNORE/REPLACE?
@@ -1184,6 +1144,8 @@ private function translate_token( WP_MySQL_Token $token ) {
case WP_MySQL_Lexer::EOF:
return null;
case WP_MySQL_Lexer::IDENTIFIER:
+ case WP_MySQL_Lexer::BACK_TICK_QUOTED_ID:
+ // @TODO: Properly unquote (MySQL) and escape (SQLite).
return '"' . trim( $token->value, '`"' ) . '"';
case WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL:
return 'AUTOINCREMENT';
@@ -1211,6 +1173,150 @@ private function translate_sequence( array $nodes, string $separator = ' ' ): ?s
return implode( $separator, $parts );
}
+ private function get_sqlite_create_table_statement( string $table_name, ?string $new_table_name = null ): array {
+ // 1. Get table info.
+ $table_info = $this->execute_sqlite_query(
+ '
+ SELECT *
+ FROM _mysql_information_schema_tables
+ WHERE table_type = "BASE TABLE"
+ AND table_schema = ?
+ AND table_name = ?
+ ',
+ array( $this->db_name, $table_name )
+ )->fetch( PDO::FETCH_ASSOC );
+
+ if ( false === $table_info ) {
+ throw new Exception( 'Table not found in information_schema' );
+ }
+
+ // 2. Get column info.
+ $column_info = $this->execute_sqlite_query(
+ 'SELECT * FROM _mysql_information_schema_columns WHERE table_schema = ? AND table_name = ?',
+ array( $this->db_name, $table_name )
+ )->fetchAll( PDO::FETCH_ASSOC );
+
+ // 3. Get index info, grouped by index name.
+ $constraint_info = $this->execute_sqlite_query(
+ 'SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = ? AND table_name = ?',
+ array( $this->db_name, $table_name )
+ )->fetchAll( PDO::FETCH_ASSOC );
+
+ $grouped_constraints = array();
+ foreach ( $constraint_info as $constraint ) {
+ $name = $constraint['INDEX_NAME'];
+ $seq = $constraint['SEQ_IN_INDEX'];
+ $grouped_constraints[ $name ][ $seq ] = $constraint;
+ }
+
+ // 4. Generate CREATE TABLE statement columns.
+ $rows = array();
+ $has_autoincrement = false;
+ foreach ( $column_info as $column ) {
+ $sql = ' ';
+ $sql .= sprintf( '"%s"', str_replace( '"', '""', $column['COLUMN_NAME'] ) );
+
+ $type = self::DATA_TYPE_STRING_MAP[ $column['DATA_TYPE'] ];
+
+ /*
+ * In SQLite, there is a PRIMARY KEY quirk for backward compatibility.
+ * This applies to ROWID tables and single-column primary keys only:
+ * 1. "INTEGER PRIMARY KEY" creates an alias of ROWID.
+ * 2. "INT PRIMARY KEY" will not alias of ROWID.
+ *
+ * Therefore, we want to:
+ * 1. Use "INT PRIMARY KEY" when we have a single-column integer
+ * PRIMARY KEY without AUTOINCREMENT (to avoid the ROWID alias).
+ * 2. Use "INTEGER PRIMARY KEY" otherwise.
+ *
+ * See:
+ * - https://www.sqlite.org/autoinc.html
+ * - https://www.sqlite.org/lang_createtable.html
+ */
+ if (
+ 'INTEGER' === $type
+ && 'PRI' === $column['COLUMN_KEY']
+ && 'auto_increment' !== $column['EXTRA']
+ && count( $grouped_constraints['PRIMARY'] ) === 1
+ ) {
+ $type = 'INT';
+ }
+
+ $sql .= ' ' . $type;
+ if ( 'NO' === $column['IS_NULLABLE'] ) {
+ $sql .= ' NOT NULL';
+ }
+ if ( 'auto_increment' === $column['EXTRA'] ) {
+ $has_autoincrement = true;
+ $sql .= ' PRIMARY KEY AUTOINCREMENT';
+ }
+ if ( null !== $column['COLUMN_DEFAULT'] ) {
+ // @TODO: Correctly quote based on the data type.
+ $sql .= ' DEFAULT ' . $this->pdo->quote( $column['COLUMN_DEFAULT'] );
+ }
+ $rows[] = $sql;
+ }
+
+ // 4. Generate CREATE TABLE statement constraints, collect indexes.
+ $create_index_sqls = array();
+ foreach ( $grouped_constraints as $constraint ) {
+ ksort( $constraint );
+ $info = $constraint[1];
+
+ if ( 'PRIMARY' === $info['INDEX_NAME'] ) {
+ if ( $has_autoincrement ) {
+ continue;
+ }
+ $sql = ' PRIMARY KEY (';
+ $sql .= implode(
+ ', ',
+ array_map(
+ function ( $column ) {
+ return sprintf( '"%s"', str_replace( '"', '""', $column['COLUMN_NAME'] ) );
+ },
+ $constraint
+ )
+ );
+ $sql .= ')';
+ $rows[] = $sql;
+ } else {
+ $is_unique = '0' === $info['NON_UNIQUE'];
+
+ $sql = sprintf( 'CREATE %sINDEX', $is_unique ? 'UNIQUE ' : '' );
+ $sql .= sprintf( ' "%s"', $info['INDEX_NAME'] );
+ $sql .= sprintf( ' ON "%s" (', $table_name );
+ $sql .= implode(
+ ', ',
+ array_map(
+ function ( $column ) {
+ return sprintf( '"%s"', str_replace( '"', '""', $column['COLUMN_NAME'] ) );
+ },
+ $constraint
+ )
+ );
+ $sql .= ')';
+
+ $create_index_sqls[] = $sql;
+ }
+ }
+
+ // 5. Compose the CREATE TABLE statement.
+ $sql = sprintf( 'CREATE TABLE "%s" (%s', str_replace( '"', '""', $new_table_name ?? $table_name ), "\n" );
+ $sql .= implode( ",\n", $rows );
+ $sql .= "\n)";
+ return array_merge( array( $sql ), $create_index_sqls );
+ }
+
+ private function unquote_sqlite_identifier( string $quoted_identifier ): string {
+ $first_byte = $quoted_identifier[0] ?? null;
+ if ( '"' === $first_byte ) {
+ $unquoted = substr( $quoted_identifier, 1, -1 );
+ } else {
+ $unquoted = $quoted_identifier;
+ }
+ return str_replace( '""', '"', $unquoted );
+ }
+
/**
* This method makes database directory and .htaccess file.
*
From 4b4fb8fa29b28f96b2555688f1dd48be1d439b19 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 20 Dec 2024 12:24:15 +0100
Subject: [PATCH 027/124] Execute ALTER TABLE using information schema
---
.../sqlite-ast/class-wp-sqlite-driver.php | 147 +++++++++++++-----
1 file changed, 109 insertions(+), 38 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index f1a63ee..684e95c 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -995,8 +995,115 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void {
}
private function execute_alter_table_statement( WP_Parser_Node $node ): void {
- $table_name = $this->translate( $node->get_descendant_node( 'tableRef' ) );
- $actions = $node->get_descendant_nodes( 'alterListItem' );
+ $table_name = $this->unquote_sqlite_identifier(
+ $this->translate( $node->get_descendant_node( 'tableRef' ) )
+ );
+
+ // Save all column names from the original table.
+ $column_names = $this->execute_sqlite_query(
+ 'SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = ? AND table_name = ?',
+ array( $this->db_name, $table_name )
+ )->fetchAll( PDO::FETCH_COLUMN );
+
+ // Track column renames and removals.
+ $column_map = array_combine( $column_names, $column_names );
+ foreach ( $node->get_descendant_nodes( 'alterListItem' ) as $action ) {
+ $first_token = $action->get_child_token();
+
+ if ( WP_MySQL_Lexer::DROP_SYMBOL === $first_token->id ) {
+ $name = $this->translate( $action->get_child_node( 'columnInternalRef' ) );
+ if ( null !== $name ) {
+ $name = $this->unquote_sqlite_identifier( $name );
+ unset( $column_map[ $name ] );
+ }
+ }
+
+ if ( WP_MySQL_Lexer::CHANGE_SYMBOL === $first_token->id ) {
+ $old_name = $this->unquote_sqlite_identifier(
+ $this->translate( $action->get_child_node( 'columnInternalRef' ) )
+ );
+ $new_name = $this->unquote_sqlite_identifier(
+ $this->translate( $action->get_child_node( 'identifier' ) )
+ );
+
+ $column_map[ $old_name ] = $new_name;
+ }
+
+ if ( WP_MySQL_Lexer::RENAME_SYMBOL === $first_token->id ) {
+ $column_ref = $action->get_child_node( 'columnInternalRef' );
+ if ( null !== $column_ref ) {
+ $old_name = $this->unquote_sqlite_identifier(
+ $this->translate( $column_ref )
+ );
+ $new_name = $this->unquote_sqlite_identifier(
+ $this->translate( $action->get_child_node( 'identifier' ) )
+ );
+
+ $column_map[ $old_name ] = $new_name;
+ }
+ }
+ }
+
+ $this->information_schema_builder->record_alter_table( $node );
+
+ /*
+ * See:
+ * https://www.sqlite.org/lang_altertable.html#making_other_kinds_of_table_schema_changes
+ */
+
+ // 1. If foreign key constraints are enabled, disable them.
+ // @TODO
+
+ // 2. Create a new table with the new schema.
+ $tmp_table_name = "_tmp__{$table_name}_" . uniqid();
+ $queries = $this->get_sqlite_create_table_statement( $table_name, $tmp_table_name );
+ $create_table_query = $queries[0];
+ $constraint_queries = array_slice( $queries, 1 );
+ $this->execute_sqlite_query( $create_table_query );
+
+ // 3. Copy data from the original table to the new table.
+ $this->execute_sqlite_query(
+ sprintf(
+ 'INSERT INTO "%s" (%s) SELECT %s FROM "%s"',
+ $tmp_table_name,
+ implode(
+ ', ',
+ array_map(
+ function ( $column ) {
+ return '"' . $column . '"';
+ },
+ $column_map
+ )
+ ),
+ implode(
+ ', ',
+ array_map(
+ function ( $column ) {
+ return '"' . $column . '"';
+ },
+ array_keys( $column_map )
+ )
+ ),
+ $table_name
+ )
+ );
+
+ // 4. Drop the original table.
+ $this->execute_sqlite_query( sprintf( 'DROP TABLE "%s"', $table_name ) );
+
+ // 5. Rename the new table to the original table name.
+ $this->execute_sqlite_query( sprintf( 'ALTER TABLE "%s" RENAME TO "%s"', $tmp_table_name, $table_name ) );
+
+ // 6. Reconstruct indexes, triggers, and views.
+ foreach ( $constraint_queries as $query ) {
+ $this->execute_sqlite_query( $query );
+ }
+
+ // @TODO: Triggers and views.
+
+ $this->results = 1;
+ $this->return_value = $this->results;
+ return;
/*
* SQLite supports only a small subset of MySQL ALTER TABLE statement.
@@ -1018,42 +1125,6 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void {
*
* @TODO: Address these nuances.
*/
- foreach ( $actions as $action ) {
- $token = $action->get_child_token();
-
- // ADD column/constraint.
- if ( WP_MySQL_Lexer::ADD_SYMBOL === $token->id ) {
- // ADD COLUMN.
- $field_definition = $action->get_descendant_node( 'fieldDefinition' );
- if ( null !== $field_definition ) {
- $field_name = $this->translate( $action->get_child_node( 'identifier' ) );
- $field = $this->translate( $field_definition );
- $this->execute_sqlite_query(
- sprintf( 'ALTER TABLE %s ADD COLUMN %s %s', $table_name, $field_name, $field )
- );
- }
-
- // ADD CONSTRAINT.
- $constraint = $action->get_descendant_node( 'tableConstraintDef' );
- if ( null !== $constraint ) {
- $constraint_name = $this->translate( $constraint->get_child_node( 'identifier' ) );
- $constraint = $this->translate( $constraint );
- $this->execute_sqlite_query(
- sprintf( 'ALTER TABLE %s ADD CONSTRAINT %s %s', $table_name, $constraint_name, $constraint )
- );
- }
- } elseif ( WP_MySQL_Lexer::DROP_SYMBOL === $token->id ) {
- // DROP COLUMN.
- $field_name = $this->translate( $action->get_child_node( 'columnInternalRef' ) );
- if ( null !== $field_name ) {
- $this->execute_sqlite_query(
- sprintf( 'ALTER TABLE %s DROP COLUMN %s', $table_name, $field_name )
- );
- }
- }
- }
-
- $this->set_result_from_affected_rows();
}
private function translate( $ast ) {
From a0a67c05538f946633331c2fd378c765c333a371 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 20 Dec 2024 12:28:55 +0100
Subject: [PATCH 028/124] Implement SHOW CREATE TABLE using information schema
---
tests/WP_SQLite_Driver_Tests.php | 78 ++++-----
.../sqlite-ast/class-wp-sqlite-driver.php | 150 ++++++++++++++++++
2 files changed, 189 insertions(+), 39 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index cd3c3ed..471782c 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -288,13 +288,13 @@ public function testShowCreateTable1() {
# TODO: Should we fix mismatch with original `option_value` text NOT NULL,` without default?
$this->assertEquals(
"CREATE TABLE `_tmp_table` (
- `ID` bigint NOT NULL AUTO_INCREMENT,
- `option_name` varchar(255) DEFAULT '',
- `option_value` text NOT NULL DEFAULT '',
- PRIMARY KEY (`ID`),
- KEY `composite` (`option_name`, `option_value`),
- UNIQUE KEY `option_name` (`option_name`)
-);",
+ `ID` bigint NOT NULL AUTO_INCREMENT,
+ `option_name` varchar(255) DEFAULT '',
+ `option_value` text NOT NULL,
+ PRIMARY KEY (`ID`),
+ UNIQUE KEY `option_name` (`option_name`),
+ KEY `composite` (`option_name`, `option_value`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci",
$results[0]->{'Create Table'}
);
}
@@ -317,13 +317,13 @@ public function testShowCreateTableQuoted() {
# TODO: Should we fix mismatch with original `option_value` text NOT NULL,` without default?
$this->assertEquals(
"CREATE TABLE `_tmp_table` (
- `ID` bigint NOT NULL AUTO_INCREMENT,
- `option_name` varchar(255) DEFAULT '',
- `option_value` text NOT NULL DEFAULT '',
- PRIMARY KEY (`ID`),
- KEY `composite` (`option_name`, `option_value`),
- UNIQUE KEY `option_name` (`option_name`)
-);",
+ `ID` bigint NOT NULL AUTO_INCREMENT,
+ `option_name` varchar(255) DEFAULT '',
+ `option_value` text NOT NULL,
+ PRIMARY KEY (`ID`),
+ UNIQUE KEY `option_name` (`option_name`),
+ KEY `composite` (`option_name`, `option_value`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci",
$results[0]->{'Create Table'}
);
}
@@ -341,8 +341,8 @@ public function testShowCreateTableSimpleTable() {
$results = $this->engine->get_query_results();
$this->assertEquals(
'CREATE TABLE `_tmp_table` (
- `ID` bigint NOT NULL DEFAULT 0
-);',
+ `ID` bigint NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci',
$results[0]->{'Create Table'}
);
}
@@ -370,12 +370,12 @@ public function testShowCreateTableWithAlterAndCreateIndex() {
$results = $this->engine->get_query_results();
$this->assertEquals(
'CREATE TABLE `_tmp_table` (
- `ID` bigint NOT NULL AUTO_INCREMENT,
- `option_name` smallint NOT NULL DEFAULT 14,
- `option_value` text NOT NULL DEFAULT \'\',
- PRIMARY KEY (`ID`),
- KEY `option_name` (`option_name`)
-);',
+ `ID` bigint NOT NULL AUTO_INCREMENT,
+ `option_name` smallint NOT NULL DEFAULT \'14\',
+ `option_value` text NOT NULL,
+ PRIMARY KEY (`ID`),
+ KEY `option_name` (`option_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci',
$results[0]->{'Create Table'}
);
}
@@ -419,13 +419,13 @@ public function testShowCreateTablePreservesDoubleUnderscoreKeyNames() {
$results = $this->engine->get_query_results();
$this->assertEquals(
'CREATE TABLE `_tmp__table` (
- `ID` bigint NOT NULL AUTO_INCREMENT,
- `option_name` varchar(255) DEFAULT \'\',
- `option_value` text NOT NULL DEFAULT \'\',
- PRIMARY KEY (`ID`),
- KEY `double__underscores` (`option_name`, `ID`),
- KEY `option_name` (`option_name`)
-);',
+ `ID` bigint NOT NULL AUTO_INCREMENT,
+ `option_name` varchar(255) DEFAULT \'\',
+ `option_value` text NOT NULL,
+ PRIMARY KEY (`ID`),
+ KEY `option_name` (`option_name`),
+ KEY `double__underscores` (`option_name`, `ID`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci',
$results[0]->{'Create Table'}
);
}
@@ -446,11 +446,11 @@ public function testShowCreateTableWithPrimaryKeyColumnsReverseOrdered() {
$results = $this->engine->get_query_results();
$this->assertEquals(
'CREATE TABLE `_tmp_table` (
- `ID_A` bigint NOT NULL DEFAULT 0,
- `ID_B` bigint NOT NULL DEFAULT 0,
- `ID_C` bigint NOT NULL DEFAULT 0,
- PRIMARY KEY (`ID_B`, `ID_A`, `ID_C`)
-);',
+ `ID_A` bigint NOT NULL,
+ `ID_B` bigint NOT NULL,
+ `ID_C` bigint NOT NULL,
+ PRIMARY KEY (`ID_B`, `ID_A`, `ID_C`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci',
$results[0]->{'Create Table'}
);
}
@@ -481,11 +481,11 @@ public function testShowCreateTableWithCorrectDefaultValues() {
$results = $this->engine->get_query_results();
$this->assertEquals(
'CREATE TABLE `_tmp__table` (
- `ID` bigint NOT NULL AUTO_INCREMENT,
- `default_empty_string` varchar(255) DEFAULT \'\',
- `null_no_default` varchar(255),
- PRIMARY KEY (`ID`)
-);',
+ `ID` bigint NOT NULL AUTO_INCREMENT,
+ `default_empty_string` varchar(255) DEFAULT \'\',
+ `null_no_default` varchar(255),
+ PRIMARY KEY (`ID`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci',
$results[0]->{'Create Table'}
);
}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 684e95c..a4c98a1 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -913,6 +913,10 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
*/
$this->results = 0;
break;
+ case 'showStatement':
+ $this->query_type = 'SHOW';
+ $this->execute_show_statement( $ast );
+ break;
default:
throw new Exception( sprintf( 'Unsupported statement type: "%s"', $ast->rule_name ) );
}
@@ -1127,6 +1131,38 @@ function ( $column ) {
*/
}
+ private function execute_show_statement( WP_Parser_Node $node ): void {
+ $tokens = $node->get_child_tokens();
+ $keyword1 = $tokens[1];
+ $keyword2 = $tokens[2] ?? null;
+
+ switch ( $keyword1->id ) {
+ case WP_MySQL_Lexer::CREATE_SYMBOL:
+ if ( WP_MySQL_Lexer::TABLE_SYMBOL === $keyword2->id ) {
+ $table_name = $this->unquote_sqlite_identifier(
+ $this->translate( $node->get_child_node( 'tableRef' ) )
+ );
+
+ $sql = $this->get_mysql_create_table_statement( $table_name );
+ if ( null === $sql ) {
+ $this->set_results_from_fetched_data( array() );
+ } else {
+ $this->set_results_from_fetched_data(
+ array(
+ (object) array(
+ 'Create Table' => $sql,
+ ),
+ )
+ );
+ }
+ return;
+ }
+ // Fall through to default.
+ default:
+ // @TODO
+ }
+ }
+
private function translate( $ast ) {
if ( null === $ast ) {
return null;
@@ -1378,6 +1414,120 @@ function ( $column ) {
return array_merge( array( $sql ), $create_index_sqls );
}
+ private function get_mysql_create_table_statement( string $table_name ): ?string {
+ // 1. Get table info.
+ $table_info = $this->execute_sqlite_query(
+ '
+ SELECT *
+ FROM _mysql_information_schema_tables
+ WHERE table_type = "BASE TABLE"
+ AND table_schema = ?
+ AND table_name = ?
+ ',
+ array( $this->db_name, $table_name )
+ )->fetch( PDO::FETCH_ASSOC );
+
+ if ( false === $table_info ) {
+ return null;
+ }
+
+ // 2. Get column info.
+ $column_info = $this->execute_sqlite_query(
+ 'SELECT * FROM _mysql_information_schema_columns WHERE table_schema = ? AND table_name = ?',
+ array( $this->db_name, $table_name )
+ )->fetchAll( PDO::FETCH_ASSOC );
+
+ // 3. Get index info, grouped by index name.
+ $constraint_info = $this->execute_sqlite_query(
+ 'SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = ? AND table_name = ?',
+ array( $this->db_name, $table_name )
+ )->fetchAll( PDO::FETCH_ASSOC );
+
+ $grouped_constraints = array();
+ foreach ( $constraint_info as $constraint ) {
+ $name = $constraint['INDEX_NAME'];
+ $seq = $constraint['SEQ_IN_INDEX'];
+ $grouped_constraints[ $name ][ $seq ] = $constraint;
+ }
+
+ // 4. Generate CREATE TABLE statement columns.
+ $rows = array();
+ foreach ( $column_info as $column ) {
+ $sql = ' ';
+ // @TODO: Proper identifier escaping.
+ $sql .= sprintf( '`%s`', str_replace( '`', '``', $column['COLUMN_NAME'] ) );
+
+ $sql .= ' ' . $column['COLUMN_TYPE'];
+ if ( 'NO' === $column['IS_NULLABLE'] ) {
+ $sql .= ' NOT NULL';
+ }
+ if ( 'auto_increment' === $column['EXTRA'] ) {
+ $sql .= ' AUTO_INCREMENT';
+ }
+ if ( null !== $column['COLUMN_DEFAULT'] ) {
+ // @TODO: Correctly quote based on the data type.
+ $sql .= ' DEFAULT ' . $this->pdo->quote( $column['COLUMN_DEFAULT'] );
+ }
+ $rows[] = $sql;
+ }
+
+ // 4. Generate CREATE TABLE statement constraints, collect indexes.
+ foreach ( $grouped_constraints as $constraint ) {
+ ksort( $constraint );
+ $info = $constraint[1];
+
+ if ( 'PRIMARY' === $info['INDEX_NAME'] ) {
+ $sql = ' PRIMARY KEY (';
+ $sql .= implode(
+ ', ',
+ array_map(
+ function ( $column ) {
+ // @TODO: Proper identifier escaping.
+ return sprintf( '`%s`', str_replace( '`', '``', $column['COLUMN_NAME'] ) );
+ },
+ $constraint
+ )
+ );
+ $sql .= ')';
+ $rows[] = $sql;
+ } else {
+ $is_unique = '0' === $info['NON_UNIQUE'];
+
+ $sql = sprintf( ' %sKEY', $is_unique ? 'UNIQUE ' : '' );
+ // @TODO: Proper identifier escaping.
+ $sql .= sprintf( ' `%s`', str_replace( '`', '``', $info['INDEX_NAME'] ) );
+ $sql .= ' (';
+ $sql .= implode(
+ ', ',
+ array_map(
+ function ( $column ) {
+ // @TODO: Proper identifier escaping.
+ return sprintf( '`%s`', str_replace( '`', '``', $column['COLUMN_NAME'] ) );
+ },
+ $constraint
+ )
+ );
+ $sql .= ')';
+
+ $rows[] = $sql;
+ }
+ }
+
+ // 5. Compose the CREATE TABLE statement.
+ // @TODO: Proper identifier escaping.
+ $sql = sprintf( 'CREATE TABLE `%s` (%s', str_replace( '`', '``', $table_name ), "\n" );
+ $sql .= implode( ",\n", $rows );
+ $sql .= "\n)";
+
+ $sql .= sprintf( ' ENGINE=%s', $table_info['ENGINE'] );
+
+ $collation = $table_info['TABLE_COLLATION'];
+ $charset = substr( $collation, 0, strpos( $collation, '_' ) );
+ $sql .= sprintf( ' DEFAULT CHARSET=%s', $charset );
+ $sql .= sprintf( ' COLLATE=%s', $collation );
+ return $sql;
+ }
+
private function unquote_sqlite_identifier( string $quoted_identifier ): string {
$first_byte = $quoted_identifier[0] ?? null;
if ( '"' === $first_byte ) {
From 837655c177d9ea26ddee1f7485595931f2c0c18d Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 20 Dec 2024 12:31:21 +0100
Subject: [PATCH 029/124] Implement SHOW INDEX using information schema
---
.../sqlite-ast/class-wp-sqlite-driver.php | 37 +++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index a4c98a1..a3a96c2 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1158,11 +1158,48 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
return;
}
// Fall through to default.
+ case WP_MySQL_Lexer::INDEX_SYMBOL:
+ case WP_MySQL_Lexer::INDEXES_SYMBOL:
+ case WP_MySQL_Lexer::KEYS_SYMBOL:
+ $table_name = $this->unquote_sqlite_identifier(
+ $this->translate( $node->get_child_node( 'tableRef' ) )
+ );
+ $this->execute_show_index_statement( $table_name );
+ break;
default:
// @TODO
}
}
+ private function execute_show_index_statement( string $table_name ): void {
+ $index_info = $this->execute_sqlite_query(
+ '
+ SELECT
+ TABLE_NAME AS `Table`,
+ NON_UNIQUE AS `Non_unique`,
+ INDEX_NAME AS `Key_name`,
+ SEQ_IN_INDEX AS `Seq_in_index`,
+ COLUMN_NAME AS `Column_name`,
+ COLLATION AS `Collation`,
+ CARDINALITY AS `Cardinality`,
+ SUB_PART AS `Sub_part`,
+ PACKED AS `Packed`,
+ NULLABLE AS `Null`,
+ INDEX_TYPE AS `Index_type`,
+ COMMENT AS `Comment`,
+ INDEX_COMMENT AS `Index_comment`,
+ IS_VISIBLE AS `Visible`,
+ EXPRESSION AS `Expression`
+ FROM _mysql_information_schema_statistics
+ WHERE table_schema = ?
+ AND table_name = ?
+ ',
+ array( $this->db_name, $table_name )
+ )->fetchAll( PDO::FETCH_OBJ );
+
+ $this->set_results_from_fetched_data( $index_info );
+ }
+
private function translate( $ast ) {
if ( null === $ast ) {
return null;
From f11fb09b413cb4171d5a1a5ebe00fd48c9e66b7b Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 20 Dec 2024 12:32:09 +0100
Subject: [PATCH 030/124] Implement SHOW GRANTS
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index a3a96c2..0d2d73d 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1166,6 +1166,15 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
);
$this->execute_show_index_statement( $table_name );
break;
+ case WP_MySQL_Lexer::GRANTS_SYMBOL:
+ $this->set_results_from_fetched_data(
+ array(
+ (object) array(
+ 'Grants for root@localhost' => 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON *.* TO `root`@`localhost` WITH GRANT OPTION',
+ ),
+ )
+ );
+ return;
default:
// @TODO
}
From cf6008d50fbe26262cc68d588bffaf687973fd9f Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 20 Dec 2024 12:32:33 +0100
Subject: [PATCH 031/124] Implement DESCRIBE using information schema
---
tests/WP_SQLite_Driver_Tests.php | 72 ++++++++++---------
.../sqlite-ast/class-wp-sqlite-driver.php | 41 +++++++++++
2 files changed, 80 insertions(+), 33 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 471782c..2e658cb 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -724,14 +724,14 @@ public function testCreateTable() {
'Type' => 'bigint(20) unsigned',
'Null' => 'NO',
'Key' => 'PRI',
- 'Default' => '0',
- 'Extra' => '',
+ 'Default' => null,
+ 'Extra' => 'auto_increment',
),
(object) array(
'Field' => 'user_login',
'Type' => 'varchar(60)',
'Null' => 'NO',
- 'Key' => '',
+ 'Key' => 'MUL',
'Default' => '',
'Extra' => '',
),
@@ -747,7 +747,7 @@ public function testCreateTable() {
'Field' => 'user_nicename',
'Type' => 'varchar(50)',
'Null' => 'NO',
- 'Key' => '',
+ 'Key' => 'MUL',
'Default' => '',
'Extra' => '',
),
@@ -755,7 +755,7 @@ public function testCreateTable() {
'Field' => 'user_email',
'Type' => 'varchar(100)',
'Null' => 'NO',
- 'Key' => '',
+ 'Key' => 'MUL',
'Default' => '',
'Extra' => '',
),
@@ -848,8 +848,8 @@ enum_column ENUM('a', 'b', 'c') NOT NULL DEFAULT 'a',
'Type' => 'bigint(20) unsigned',
'Null' => 'NO',
'Key' => 'PRI',
- 'Default' => '0',
- 'Extra' => '',
+ 'Default' => null,
+ 'Extra' => 'auto_increment',
),
(object) array(
'Field' => 'decimal_column',
@@ -1052,7 +1052,7 @@ public function testColumnWithOnUpdate() {
'Type' => 'int(11)',
'Null' => 'NO',
'Key' => '',
- 'Default' => '0',
+ 'Default' => null,
'Extra' => '',
),
(object) array(
@@ -1061,7 +1061,7 @@ public function testColumnWithOnUpdate() {
'Null' => 'YES',
'Key' => '',
'Default' => null,
- 'Extra' => '',
+ 'Extra' => 'on update CURRENT_TIMESTAMP',
),
),
$results
@@ -1079,7 +1079,7 @@ public function testColumnWithOnUpdate() {
'Type' => 'int(11)',
'Null' => 'NO',
'Key' => '',
- 'Default' => '0',
+ 'Default' => null,
'Extra' => '',
),
(object) array(
@@ -1088,7 +1088,7 @@ public function testColumnWithOnUpdate() {
'Null' => 'YES',
'Key' => '',
'Default' => null,
- 'Extra' => '',
+ 'Extra' => 'on update CURRENT_TIMESTAMP',
),
(object) array(
'Field' => 'updated_at',
@@ -1096,7 +1096,7 @@ public function testColumnWithOnUpdate() {
'Null' => 'YES',
'Key' => '',
'Default' => null,
- 'Extra' => '',
+ 'Extra' => 'on update CURRENT_TIMESTAMP',
),
),
$results
@@ -1221,7 +1221,7 @@ public function testChangeColumnWithOnUpdate() {
'Type' => 'int(11)',
'Null' => 'NO',
'Key' => '',
- 'Default' => '0',
+ 'Default' => null,
'Extra' => '',
),
(object) array(
@@ -1254,7 +1254,7 @@ public function testChangeColumnWithOnUpdate() {
'Type' => 'int(11)',
'Null' => 'NO',
'Key' => '',
- 'Default' => '0',
+ 'Default' => null,
'Extra' => '',
),
(object) array(
@@ -1263,7 +1263,7 @@ public function testChangeColumnWithOnUpdate() {
'Null' => 'YES',
'Key' => '',
'Default' => null,
- 'Extra' => '',
+ 'Extra' => 'on update CURRENT_TIMESTAMP',
),
),
$results
@@ -1286,7 +1286,7 @@ public function testChangeColumnWithOnUpdate() {
'Type' => 'int(11)',
'Null' => 'NO',
'Key' => '',
- 'Default' => '0',
+ 'Default' => null,
'Extra' => '',
),
(object) array(
@@ -1328,7 +1328,7 @@ public function testAlterTableWithColumnFirstAndAfter() {
'Type' => 'int(11)',
'Null' => 'NO',
'Key' => '',
- 'Default' => '0',
+ 'Default' => null,
'Extra' => '',
),
(object) array(
@@ -1336,7 +1336,7 @@ public function testAlterTableWithColumnFirstAndAfter() {
'Type' => 'varchar(20)',
'Null' => 'NO',
'Key' => '',
- 'Default' => null,
+ 'Default' => '',
'Extra' => '',
),
(object) array(
@@ -1363,7 +1363,7 @@ public function testAlterTableWithColumnFirstAndAfter() {
'Type' => 'int(11)',
'Null' => 'NO',
'Key' => '',
- 'Default' => '0',
+ 'Default' => null,
'Extra' => '',
),
(object) array(
@@ -1457,7 +1457,7 @@ public function testAlterTableWithColumnFirstAndAfter() {
'Type' => 'varchar(20)',
'Null' => 'NO',
'Key' => '',
- 'Default' => null,
+ 'Default' => '',
'Extra' => '',
),
(object) array(
@@ -1503,7 +1503,7 @@ public function testAlterTableWithMultiColumnFirstAndAfter() {
'Type' => 'int(11)',
'Null' => 'NO',
'Key' => '',
- 'Default' => '0',
+ 'Default' => null,
'Extra' => '',
),
(object) array(
@@ -1549,7 +1549,7 @@ public function testAlterTableWithMultiColumnFirstAndAfter() {
'Type' => 'int(11)',
'Null' => 'NO',
'Key' => '',
- 'Default' => '0',
+ 'Default' => null,
'Extra' => '',
),
(object) array(
@@ -1600,7 +1600,7 @@ public function testAlterTableAddIndex() {
'Table' => '_tmp_table',
'Non_unique' => '1',
'Key_name' => 'name',
- 'Seq_in_index' => '0',
+ 'Seq_in_index' => '1',
'Column_name' => 'name',
'Collation' => 'A',
'Cardinality' => '0',
@@ -1610,6 +1610,8 @@ public function testAlterTableAddIndex() {
'Index_type' => 'BTREE',
'Comment' => '',
'Index_comment' => '',
+ 'Visible' => 'YES',
+ 'Expression' => null,
),
),
$results
@@ -1635,16 +1637,18 @@ public function testAlterTableAddUniqueIndex() {
'Table' => '_tmp_table',
'Non_unique' => '0',
'Key_name' => 'name',
- 'Seq_in_index' => '0',
+ 'Seq_in_index' => '1',
'Column_name' => 'name',
'Collation' => 'A',
'Cardinality' => '0',
- 'Sub_part' => null,
+ 'Sub_part' => '20',
'Packed' => null,
'Null' => '',
'Index_type' => 'BTREE',
'Comment' => '',
'Index_comment' => '',
+ 'Visible' => 'YES',
+ 'Expression' => null,
),
),
$results
@@ -1670,9 +1674,9 @@ public function testAlterTableAddFulltextIndex() {
'Table' => '_tmp_table',
'Non_unique' => '1',
'Key_name' => 'name',
- 'Seq_in_index' => '0',
+ 'Seq_in_index' => '1',
'Column_name' => 'name',
- 'Collation' => 'A',
+ 'Collation' => null,
'Cardinality' => '0',
'Sub_part' => null,
'Packed' => null,
@@ -1680,6 +1684,8 @@ public function testAlterTableAddFulltextIndex() {
'Index_type' => 'FULLTEXT',
'Comment' => '',
'Index_comment' => '',
+ 'Visible' => 'YES',
+ 'Expression' => null,
),
),
$results
@@ -2167,15 +2173,14 @@ public function testNestedTransactionWorkComplexModify() {
$fields = $this->engine->get_query_results();
$this->assertEquals(
- $fields,
array(
(object) array(
'Field' => 'ID',
- 'Type' => 'integer',
+ 'Type' => 'int',
'Null' => 'NO',
'Key' => 'PRI',
- 'Default' => '0',
- 'Extra' => '',
+ 'Default' => null,
+ 'Extra' => 'auto_increment',
),
(object) array(
'Field' => 'option_name',
@@ -2193,7 +2198,8 @@ public function testNestedTransactionWorkComplexModify() {
'Default' => '',
'Extra' => '',
),
- )
+ ),
+ $fields
);
}
@@ -2426,7 +2432,7 @@ public function testDescribeAccurate() {
'Field' => 'term_name',
'Type' => 'varchar(11)',
'Null' => 'NO',
- 'Key' => '',
+ 'Key' => 'MUL',
'Default' => '0',
'Extra' => '',
),
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 0d2d73d..58a7bd4 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -917,6 +917,23 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$this->query_type = 'SHOW';
$this->execute_show_statement( $ast );
break;
+ case 'utilityStatement':
+ $this->query_type = 'DESCRIBE';
+ $subtree = $ast->get_child_node();
+ switch ( $subtree->rule_name ) {
+ case 'describeStatement':
+ $this->execute_describe_statement( $subtree );
+ break;
+ default:
+ throw new Exception(
+ sprintf(
+ 'Unsupported statement type: "%s" > "%s"',
+ $ast->rule_name,
+ $subtree->rule_name
+ )
+ );
+ }
+ break;
default:
throw new Exception( sprintf( 'Unsupported statement type: "%s"', $ast->rule_name ) );
}
@@ -1209,6 +1226,30 @@ private function execute_show_index_statement( string $table_name ): void {
$this->set_results_from_fetched_data( $index_info );
}
+ private function execute_describe_statement( WP_Parser_Node $node ): void {
+ $table_name = $this->unquote_sqlite_identifier(
+ $this->translate( $node->get_child_node( 'tableRef' ) )
+ );
+
+ $column_info = $this->execute_sqlite_query(
+ '
+ SELECT
+ column_name AS `Field`,
+ column_type AS `Type`,
+ is_nullable AS `Null`,
+ column_key AS `Key`,
+ column_default AS `Default`,
+ extra AS Extra
+ FROM _mysql_information_schema_columns
+ WHERE table_schema = ?
+ AND table_name = ?
+ ',
+ array( $this->db_name, $table_name )
+ )->fetchAll( PDO::FETCH_OBJ );
+
+ $this->set_results_from_fetched_data( $column_info );
+ }
+
private function translate( $ast ) {
if ( null === $ast ) {
return null;
From f5a52cadc6249ece1a9a83388abc71b455793bd8 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 20 Dec 2024 12:32:57 +0100
Subject: [PATCH 032/124] Ignore index hints
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 3 +++
1 file changed, 3 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 58a7bd4..30752a0 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1328,6 +1328,9 @@ private function translate( $ast ) {
// The "AS" keyword is optional in MySQL, but required in SQLite.
return 'AS ' . $this->translate( $ast->get_child_node() );
+ case 'indexHint':
+ case 'indexHintList':
+ return null;
default:
return $this->translate_sequence( $ast->get_children() );
}
From 174916ac7736a7465f10845cd90bebe55e73aa88 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 10 Jan 2025 10:09:42 +0100
Subject: [PATCH 033/124] Add support for LIKE BINARY
---
tests/WP_SQLite_Driver_Tests.php | 119 ++++++++-----
.../sqlite-ast/class-wp-sqlite-driver.php | 46 +++++
...s-wp-sqlite-pdo-user-defined-functions.php | 163 +++++++++++++-----
3 files changed, 250 insertions(+), 78 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 2e658cb..13bbfcf 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -3033,13 +3033,13 @@ public function testTranslatesUtf8SELECT() {
$this->assertQuery( 'DELETE FROM _options' );
}
- public function testTranslateLikeBinaryAndGlob() {
+ public function testTranslateLikeBinary() {
// Create a temporary table for testing
$this->assertQuery(
- "CREATE TABLE _tmp_table (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
- name varchar(20) NOT NULL default ''
- );"
+ 'CREATE TABLE _tmp_table (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ name varchar(20)
+ )'
);
// Insert data into the table
@@ -3052,70 +3052,111 @@ public function testTranslateLikeBinaryAndGlob() {
$this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special%chars');" );
$this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special_chars');" );
$this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special\\chars');" );
+ $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('aste*risk');" );
+ $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('question?mark');" );
- // Test case-sensitive LIKE BINARY
+ // Test exact string
$result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'first'" );
$this->assertCount( 1, $result );
$this->assertEquals( 'first', $result[0]->name );
- // Test case-sensitive LIKE BINARY with wildcard %
+ // Test exact string with no matches
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'third'" );
+ $this->assertCount( 0, $result );
+
+ // Test mixed case
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'First'" );
+ $this->assertCount( 0, $result );
+
+ // Test % wildcard
$result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'f%'" );
$this->assertCount( 1, $result );
$this->assertEquals( 'first', $result[0]->name );
- // Test case-sensitive LIKE BINARY with wildcard _
+ // Test % wildcard with no matches
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'x%'" );
+ $this->assertCount( 0, $result );
+
+ // Test "%" character (not a wildcard)
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'special\\%chars'" );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 'special%chars', $result[0]->name );
+
+ // Test _ wildcard
$result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'f_rst'" );
$this->assertCount( 1, $result );
$this->assertEquals( 'first', $result[0]->name );
- // Test case-insensitive LIKE
- $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE 'FIRST'" );
- $this->assertCount( 2, $result ); // Should match both 'first' and 'FIRST'
-
- // Test mixed case with LIKE BINARY
- $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'First'" );
- $this->assertCount( 0, $result );
-
- // Test no matches with LIKE BINARY
- $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'third'" );
+ // Test _ wildcard with no matches
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'x_yz'" );
$this->assertCount( 0, $result );
- // Test GLOB equivalent for case-sensitive matching with wildcard
- $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'f*'" );
+ // Test "_" character (not a wildcard)
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'special\\_chars'" );
$this->assertCount( 1, $result );
- $this->assertEquals( 'first', $result[0]->name );
+ $this->assertEquals( 'special_chars', $result[0]->name );
- // Test GLOB with single character wildcard
- $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'f?rst'" );
+ // Test escaping of "*"
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'aste*risk'" );
$this->assertCount( 1, $result );
- $this->assertEquals( 'first', $result[0]->name );
+ $this->assertEquals( 'aste*risk', $result[0]->name );
- // Test GLOB with no matches
- $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'S*'" );
+ // Test escaping of "*" with no matches
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'f*'" );
$this->assertCount( 0, $result );
- // Test GLOB case sensitivity with LIKE and GLOB
- $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'first';" );
- $this->assertCount( 1, $result ); // Should only match 'first'
+ // Test escaping of "?"
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'question?mark'" );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 'question?mark', $result[0]->name );
- $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'FIRST';" );
- $this->assertCount( 1, $result ); // Should only match 'FIRST'
+ // Test escaping of "?" with no matches
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'f?rst'" );
+ $this->assertCount( 0, $result );
- // Test NULL comparison with LIKE BINARY
- $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'first';" );
- $this->assertCount( 1, $result );
- $this->assertEquals( 'first', $result[0]->name );
+ // Test escaping of character class
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY '[f]irst'" );
+ $this->assertCount( 0, $result );
- $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE name LIKE BINARY NULL;' );
- $this->assertCount( 0, $result ); // NULL comparison should return no results
+ // Test NULL
+ $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE name LIKE BINARY NULL' );
+ $this->assertCount( 0, $result );
// Test pattern with special characters using LIKE BINARY
- $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY '%special%';" );
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY '%special%'" );
$this->assertCount( 4, $result );
$this->assertEquals( '%special%', $result[0]->name );
$this->assertEquals( 'special%chars', $result[1]->name );
$this->assertEquals( 'special_chars', $result[2]->name );
- $this->assertEquals( 'specialchars', $result[3]->name );
+ $this->assertEquals( 'special\chars', $result[3]->name );
+
+ // Test escaping - "\t" is a tab character
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'firs\\t'" );
+ $this->assertCount( 0, $result );
+
+ // Test escaping - "\\t" is "t" (input resolves to "\t", which LIKE resolves to "t")
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'firs\\\\t'" );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 'first', $result[0]->name );
+
+ // Test escaping - "\%" is a "%" literal
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'special\\%chars'" );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 'special%chars', $result[0]->name );
+
+ // Test escaping - "\\%" is also a "%" literal
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'special\\\\%chars'" );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 'special%chars', $result[0]->name );
+
+ // Test escaping - "\\\%" is "\" and a wildcard
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'special\\\\\\%chars'" );
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 'special\\chars', $result[0]->name );
+
+ // Test LIKE without BINARY
+ $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE 'FIRST'" );
+ $this->assertCount( 2, $result ); // Should match both 'first' and 'FIRST'
}
public function testOnConflictReplace() {
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 30752a0..d18d92a 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1313,6 +1313,12 @@ private function translate( $ast ) {
throw $this->not_supported_exception(
sprintf( 'data type: %s', $child->value )
);
+ case 'predicateOperations':
+ $token = $ast->get_child_token();
+ if ( WP_MySQL_Lexer::LIKE_SYMBOL === $token->id ) {
+ return $this->translate_like( $ast );
+ }
+ return $this->translate_sequence( $ast->get_children() );
case 'systemVariable':
// @TODO: Emulate some system variables, or use reasonable defaults.
// See: https://dev.mysql.com/doc/refman/8.4/en/server-system-variable-reference.html
@@ -1346,6 +1352,13 @@ private function translate_token( WP_MySQL_Token $token ) {
return '"' . trim( $token->value, '`"' ) . '"';
case WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL:
return 'AUTOINCREMENT';
+ case WP_MySQL_Lexer::BINARY_SYMBOL:
+ /*
+ * There is no "BINARY expr" equivalent in SQLite. We can look for
+ * the BINARY keyword in particular cases (with REGEXP, LIKE, etc.)
+ * and then remove it from the translated output here.
+ */
+ return null;
default:
return $token->value;
}
@@ -1370,6 +1383,39 @@ private function translate_sequence( array $nodes, string $separator = ' ' ): ?s
return implode( $separator, $parts );
}
+ private function translate_like( WP_Parser_Node $node ): string {
+ $tokens = $node->get_descendant_tokens();
+ $is_binary = isset( $tokens[1] ) && WP_MySQL_Lexer::BINARY_SYMBOL === $tokens[1]->id;
+
+ if ( true === $is_binary ) {
+ $children = $node->get_children();
+ return sprintf(
+ 'GLOB _helper_like_to_glob_pattern(%s)',
+ $this->translate( $children[1] )
+ );
+ }
+
+ /*
+ * @TODO: Implement the ESCAPE '...' clause.
+ */
+
+ /*
+ * @TODO: Implement more correct LIKE behavior.
+ *
+ * While SQLite supports the LIKE operator, it seems to differ from the
+ * MySQL behavior in some ways:
+ *
+ * 1. In SQLite, LIKE is case-insensitive only for ASCII characters
+ * ('a' LIKE 'A' is TRUE but 'æ' LIKE 'Æ' is FALSE)
+ * 2. In MySQL, LIKE interprets some escape sequences. See the contents
+ * of the "_helper_like_to_glob_pattern" function.
+ *
+ * We'll probably need to overload the like() function:
+ * https://www.sqlite.org/lang_corefunc.html#like
+ */
+ return $this->translate_sequence( $node->get_children() );
+ }
+
private function get_sqlite_create_table_statement( string $table_name, ?string $new_table_name = null ): array {
// 1. Get table info.
$table_info = $this->execute_sqlite_query(
diff --git a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php
index 6f0d83d..010e5a9 100644
--- a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php
+++ b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php
@@ -46,45 +46,48 @@ public function __construct( $pdo ) {
* @var array
*/
private $functions = array(
- 'month' => 'month',
- 'monthnum' => 'month',
- 'year' => 'year',
- 'day' => 'day',
- 'hour' => 'hour',
- 'minute' => 'minute',
- 'second' => 'second',
- 'week' => 'week',
- 'weekday' => 'weekday',
- 'dayofweek' => 'dayofweek',
- 'dayofmonth' => 'dayofmonth',
- 'unix_timestamp' => 'unix_timestamp',
- 'now' => 'now',
- 'md5' => 'md5',
- 'curdate' => 'curdate',
- 'rand' => 'rand',
- 'from_unixtime' => 'from_unixtime',
- 'localtime' => 'now',
- 'localtimestamp' => 'now',
- 'isnull' => 'isnull',
- 'if' => '_if',
- 'regexp' => 'regexp',
- 'field' => 'field',
- 'log' => 'log',
- 'least' => 'least',
- 'greatest' => 'greatest',
- 'get_lock' => 'get_lock',
- 'release_lock' => 'release_lock',
- 'ucase' => 'ucase',
- 'lcase' => 'lcase',
- 'unhex' => 'unhex',
- 'inet_ntoa' => 'inet_ntoa',
- 'inet_aton' => 'inet_aton',
- 'datediff' => 'datediff',
- 'locate' => 'locate',
- 'utc_date' => 'utc_date',
- 'utc_time' => 'utc_time',
- 'utc_timestamp' => 'utc_timestamp',
- 'version' => 'version',
+ 'month' => 'month',
+ 'monthnum' => 'month',
+ 'year' => 'year',
+ 'day' => 'day',
+ 'hour' => 'hour',
+ 'minute' => 'minute',
+ 'second' => 'second',
+ 'week' => 'week',
+ 'weekday' => 'weekday',
+ 'dayofweek' => 'dayofweek',
+ 'dayofmonth' => 'dayofmonth',
+ 'unix_timestamp' => 'unix_timestamp',
+ 'now' => 'now',
+ 'md5' => 'md5',
+ 'curdate' => 'curdate',
+ 'rand' => 'rand',
+ 'from_unixtime' => 'from_unixtime',
+ 'localtime' => 'now',
+ 'localtimestamp' => 'now',
+ 'isnull' => 'isnull',
+ 'if' => '_if',
+ 'regexp' => 'regexp',
+ 'field' => 'field',
+ 'log' => 'log',
+ 'least' => 'least',
+ 'greatest' => 'greatest',
+ 'get_lock' => 'get_lock',
+ 'release_lock' => 'release_lock',
+ 'ucase' => 'ucase',
+ 'lcase' => 'lcase',
+ 'unhex' => 'unhex',
+ 'inet_ntoa' => 'inet_ntoa',
+ 'inet_aton' => 'inet_aton',
+ 'datediff' => 'datediff',
+ 'locate' => 'locate',
+ 'utc_date' => 'utc_date',
+ 'utc_time' => 'utc_time',
+ 'utc_timestamp' => 'utc_timestamp',
+ 'version' => 'version',
+
+ // Internal helper functions.
+ '_helper_like_to_glob_pattern' => '_helper_like_to_glob_pattern',
);
/**
@@ -759,4 +762,86 @@ public function utc_timestamp() {
public function version() {
return '5.5';
}
+
+ /**
+ * A helper to covert LIKE pattern to a GLOB pattern for "LIKE BINARY" support.
+
+ * @TODO: Some of the MySQL string specifics described below are likely to
+ * affect also other patterns than just "LIKE BINARY". We should
+ * consider applying some of the conversions more broadly.
+ *
+ * @param string $pattern
+ * @return string
+ */
+ public function _helper_like_to_glob_pattern( $pattern ) {
+ if ( null === $pattern ) {
+ return null;
+ }
+
+ /*
+ * 1. 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 in cases
+ * where only one is used. To do so correctly, we need to:
+ *
+ * 1. Skip all double backslash patterns (as "\\" resolves to "\").
+ * 2. Add an extra backslash when "\%" or "\_" follows right after.
+ *
+ * This may be related to: https://bugs.mysql.com/bug.php?id=84118
+ */
+ $pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2}))*(\\\\[%_])/', '$1\\\\$2', $pattern );
+
+ /*
+ * 2. Unescape C-style escape sequences.
+ *
+ * MySQL string literals are represented using C-style encoded strings,
+ * but the GLOB pattern in SQLite doesn't support such escaping.
+ */
+ $pattern = stripcslashes( $pattern );
+
+ /*
+ * 3. Escape characters that have special meaning in GLOB patterns.
+ *
+ * We need to:
+ * 1. Escape "]" as "[]]" to avoid interpreting "[...]" as a character class.
+ * 2. Escape "*" as "[*]" (must be after 1 to avoid being escaped).
+ * 3. Escape "?" as "[?]" (must be after 1 to avoid being escaped).
+ */
+ $pattern = str_replace( ']', '[]]', $pattern );
+ $pattern = str_replace( '*', '[*]', $pattern );
+ $pattern = str_replace( '?', '[?]', $pattern );
+
+ /*
+ * 4. Convert LIKE wildcards to GLOB wildcards ("%" -> "*", "_" -> "?").
+ *
+ * We need to convert them only when they don't follow any backslashes,
+ * or when they follow an even number of backslashes (as "\\" is "\").
+ */
+ $pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)%/', '$1*', $pattern );
+ $pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)_/', '$1?', $pattern );
+
+ /*
+ * 5. Unescape LIKE escape sequences.
+ *
+ * While in MySQL LIKE patterns, a backslash is usually used to escape
+ * special characters ("%", "_", and "\"), it works with all characters.
+ *
+ * That is:
+ * SELECT '\\x' prints '\x', but LIKE '\\x' is equivalent to LIKE 'x'.
+ *
+ * This is true also for multi-byte characters:
+ * SELECT '\\©' prints '\©', but LIKE '\\©' is equivalent to LIKE '©'.
+ *
+ * However, the multi-byte behavior is likely to depend on the charset.
+ * For now, we'll assume UTF-8 and thus the "u" modifier for the regex.
+ */
+ $pattern = preg_replace( '/\\\\(.)/u', '$1', $pattern );
+
+ return $pattern;
+ }
}
From 335388b048031aa62ded67854939133a21c279d5 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 10 Jan 2025 10:10:46 +0100
Subject: [PATCH 034/124] Add support for REGEXP functions
---
.../sqlite-ast/class-wp-sqlite-driver.php | 25 +++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index d18d92a..bd23f34 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1317,6 +1317,8 @@ private function translate( $ast ) {
$token = $ast->get_child_token();
if ( WP_MySQL_Lexer::LIKE_SYMBOL === $token->id ) {
return $this->translate_like( $ast );
+ } elseif ( WP_MySQL_Lexer::REGEXP_SYMBOL === $token->id ) {
+ return $this->translate_regexp_functions( $ast );
}
return $this->translate_sequence( $ast->get_children() );
case 'systemVariable':
@@ -1416,6 +1418,29 @@ private function translate_like( WP_Parser_Node $node ): string {
return $this->translate_sequence( $node->get_children() );
}
+ private function translate_regexp_functions( WP_Parser_Node $node ): string {
+ $tokens = $node->get_descendant_tokens();
+ $is_binary = isset( $tokens[1] ) && WP_MySQL_Lexer::BINARY_SYMBOL === $tokens[1]->id;
+
+ /*
+ * If the query says REGEXP BINARY, the comparison is byte-by-byte
+ * and letter casing matters – lowercase and uppercase letters are
+ * represented using different byte codes.
+ *
+ * The REGEXP function can't be easily made to accept two
+ * parameters, so we'll have to use a hack to get around this.
+ *
+ * If the first character of the pattern is a null byte, we'll
+ * remove it and make the comparison case-sensitive. This should
+ * be reasonably safe since PHP does not allow null bytes in
+ * regular expressions anyway.
+ */
+ if ( true === $is_binary ) {
+ return 'REGEXP CHAR(0) || ' . $this->translate( $node->get_child_node() );
+ }
+ return 'REGEXP ' . $this->translate( $node->get_child_node() );
+ }
+
private function get_sqlite_create_table_statement( string $table_name, ?string $new_table_name = null ): array {
// 1. Get table info.
$table_info = $this->execute_sqlite_query(
From 86a577c1113c13fb3bdf396b416dc1c68c5377b9 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 10 Jan 2025 13:57:07 +0100
Subject: [PATCH 035/124] Add support for LEFT function
---
.../sqlite-ast/class-wp-sqlite-driver.php | 21 +++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index bd23f34..51c4f68 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1321,6 +1321,8 @@ private function translate( $ast ) {
return $this->translate_regexp_functions( $ast );
}
return $this->translate_sequence( $ast->get_children() );
+ case 'runtimeFunctionCall':
+ return $this->translate_runtime_function_call( $ast );
case 'systemVariable':
// @TODO: Emulate some system variables, or use reasonable defaults.
// See: https://dev.mysql.com/doc/refman/8.4/en/server-system-variable-reference.html
@@ -1441,6 +1443,25 @@ private function translate_regexp_functions( WP_Parser_Node $node ): string {
return 'REGEXP ' . $this->translate( $node->get_child_node() );
}
+ private function translate_runtime_function_call( WP_Parser_Node $node ): string {
+ $child = $node->get_child();
+ if ( $child instanceof WP_Parser_Node ) {
+ return $this->translate( $child );
+ }
+
+ switch ( $child->id ) {
+ case WP_MySQL_Lexer::LEFT_SYMBOL:
+ $nodes = $node->get_child_nodes();
+ return sprintf(
+ 'SUBSTRING(%s, 1, %s)',
+ $this->translate( $nodes[0] ),
+ $this->translate( $nodes[1] )
+ );
+ default:
+ return $this->translate_sequence( $node->get_children() );
+ }
+ }
+
private function get_sqlite_create_table_statement( string $table_name, ?string $new_table_name = null ): array {
// 1. Get table info.
$table_info = $this->execute_sqlite_query(
From 0b3ba08bb46d8fe0c3a1f8348c0db1b8cf7b9c83 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 10 Jan 2025 13:57:50 +0100
Subject: [PATCH 036/124] Add support for DATE_ADD and DATE_SUB functions
---
tests/WP_SQLite_Driver_Tests.php | 88 +++++++++++++++++++
.../sqlite-ast/class-wp-sqlite-driver.php | 16 ++++
2 files changed, 104 insertions(+)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 13bbfcf..e43a159 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -506,6 +506,94 @@ public function testSelectIndexHintUseGroup() {
$this->assertEquals( 1, $result[0]->output );
}
+ public function testDateAddFunction() {
+ // second
+ $result = $this->assertQuery(
+ 'SELECT DATE_ADD("2008-01-02 13:29:17", INTERVAL 1 SECOND) as output'
+ );
+ $this->assertEquals( '2008-01-02 13:29:18', $result[0]->output );
+
+ // minute
+ $result = $this->assertQuery(
+ 'SELECT DATE_ADD("2008-01-02 13:29:17", INTERVAL 1 MINUTE) as output'
+ );
+ $this->assertEquals( '2008-01-02 13:30:17', $result[0]->output );
+
+ // hour
+ $result = $this->assertQuery(
+ 'SELECT DATE_ADD("2008-01-02 13:29:17", INTERVAL 1 HOUR) as output'
+ );
+ $this->assertEquals( '2008-01-02 14:29:17', $result[0]->output );
+
+ // day
+ $result = $this->assertQuery(
+ 'SELECT DATE_ADD("2008-01-02 13:29:17", INTERVAL 1 DAY) as output'
+ );
+ $this->assertEquals( '2008-01-03 13:29:17', $result[0]->output );
+
+ // week
+ $result = $this->assertQuery(
+ 'SELECT DATE_ADD("2008-01-02 13:29:17", INTERVAL 1 WEEK) as output'
+ );
+ $this->assertEquals( '2008-01-09 13:29:17', $result[0]->output );
+
+ // month
+ $result = $this->assertQuery(
+ 'SELECT DATE_ADD("2008-01-02 13:29:17", INTERVAL 1 MONTH) as output'
+ );
+ $this->assertEquals( '2008-02-02 13:29:17', $result[0]->output );
+
+ // year
+ $result = $this->assertQuery(
+ 'SELECT DATE_ADD("2008-01-02 13:29:17", INTERVAL 1 YEAR) as output'
+ );
+ $this->assertEquals( '2009-01-02 13:29:17', $result[0]->output );
+ }
+
+ public function testDateSubFunction() {
+ // second
+ $result = $this->assertQuery(
+ 'SELECT DATE_SUB("2008-01-02 13:29:17", INTERVAL 1 SECOND) as output'
+ );
+ $this->assertEquals( '2008-01-02 13:29:16', $result[0]->output );
+
+ // minute
+ $result = $this->assertQuery(
+ 'SELECT DATE_SUB("2008-01-02 13:29:17", INTERVAL 1 MINUTE) as output'
+ );
+ $this->assertEquals( '2008-01-02 13:28:17', $result[0]->output );
+
+ // hour
+ $result = $this->assertQuery(
+ 'SELECT DATE_SUB("2008-01-02 13:29:17", INTERVAL 1 HOUR) as output'
+ );
+ $this->assertEquals( '2008-01-02 12:29:17', $result[0]->output );
+
+ // day
+ $result = $this->assertQuery(
+ 'SELECT DATE_SUB("2008-01-02 13:29:17", INTERVAL 1 DAY) as output'
+ );
+ $this->assertEquals( '2008-01-01 13:29:17', $result[0]->output );
+
+ // week
+ $result = $this->assertQuery(
+ 'SELECT DATE_SUB("2008-01-02 13:29:17", INTERVAL 1 WEEK) as output'
+ );
+ $this->assertEquals( '2007-12-26 13:29:17', $result[0]->output );
+
+ // month
+ $result = $this->assertQuery(
+ 'SELECT DATE_SUB("2008-01-02 13:29:17", INTERVAL 1 MONTH) as output'
+ );
+ $this->assertEquals( '2007-12-02 13:29:17', $result[0]->output );
+
+ // year
+ $result = $this->assertQuery(
+ 'SELECT DATE_SUB("2008-01-02 13:29:17", INTERVAL 1 YEAR) as output'
+ );
+ $this->assertEquals( '2007-01-02 13:29:17', $result[0]->output );
+ }
+
public function testLeftFunction1Char() {
$result = $this->assertQuery(
'SELECT LEFT("abc", 1) as output'
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 51c4f68..d8a1e4a 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1450,6 +1450,22 @@ private function translate_runtime_function_call( WP_Parser_Node $node ): string
}
switch ( $child->id ) {
+ case WP_MySQL_Lexer::DATE_ADD_SYMBOL:
+ case WP_MySQL_Lexer::DATE_SUB_SYMBOL:
+ $nodes = $node->get_child_nodes();
+ $value = $this->translate( $nodes[1] );
+ $unit = $this->translate( $nodes[2] );
+ if ( 'WEEK' === $unit ) {
+ $unit = 'DAY';
+ $value = 7 * $value;
+ }
+ return sprintf(
+ "DATETIME(%s, '%s' || %s || ' %s')",
+ $this->translate( $nodes[0] ),
+ WP_MySQL_Lexer::DATE_SUB_SYMBOL === $child->id ? '-' : '+',
+ $value,
+ $unit
+ );
case WP_MySQL_Lexer::LEFT_SYMBOL:
$nodes = $node->get_child_nodes();
return sprintf(
From aa9ad47976d1804aefc98b65c5f3e45ee9ed8a7b Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Mon, 13 Jan 2025 19:58:19 +0100
Subject: [PATCH 037/124] Add support for DATE_FORMAT()
---
.../sqlite-ast/class-wp-sqlite-driver.php | 100 ++++++++++++++++++
1 file changed, 100 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index d8a1e4a..3ebb3c4 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -151,6 +151,51 @@ class WP_SQLite_Driver {
'geometrycollection' => 'TEXT',
);
+ /**
+ * The MySQL to SQLite date formats translation.
+ *
+ * Maps MySQL formats to SQLite strftime() formats.
+ *
+ * For MySQL formats, see:
+ * * https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_date-format
+ *
+ * For SQLite formats, see:
+ * * https://www.sqlite.org/lang_datefunc.html
+ * * https://strftime.org/
+ */
+ const DATE_FORMAT_TO_STRFTIME_MAP = array(
+ '%a' => '%D',
+ '%b' => '%M',
+ '%c' => '%n',
+ '%D' => '%jS',
+ '%d' => '%d',
+ '%e' => '%j',
+ '%H' => '%H',
+ '%h' => '%h',
+ '%I' => '%h',
+ '%i' => '%M',
+ '%j' => '%z',
+ '%k' => '%G',
+ '%l' => '%g',
+ '%M' => '%F',
+ '%m' => '%m',
+ '%p' => '%A',
+ '%r' => '%h:%i:%s %A',
+ '%S' => '%s',
+ '%s' => '%s',
+ '%T' => '%H:%i:%s',
+ '%U' => '%W',
+ '%u' => '%W',
+ '%V' => '%W',
+ '%v' => '%W',
+ '%W' => '%l',
+ '%w' => '%w',
+ '%X' => '%Y',
+ '%x' => '%o',
+ '%Y' => '%Y',
+ '%y' => '%y',
+ );
+
const DATA_TYPES_CACHE_TABLE = '_mysql_data_types_cache';
const CREATE_DATA_TYPES_CACHE_TABLE = 'CREATE TABLE IF NOT EXISTS _mysql_data_types_cache (
@@ -1323,6 +1368,8 @@ private function translate( $ast ) {
return $this->translate_sequence( $ast->get_children() );
case 'runtimeFunctionCall':
return $this->translate_runtime_function_call( $ast );
+ case 'functionCall':
+ return $this->translate_function_call( $ast );
case 'systemVariable':
// @TODO: Emulate some system variables, or use reasonable defaults.
// See: https://dev.mysql.com/doc/refman/8.4/en/server-system-variable-reference.html
@@ -1478,6 +1525,59 @@ private function translate_runtime_function_call( WP_Parser_Node $node ): string
}
}
+ private function translate_function_call( WP_Parser_Node $node ): string {
+ $nodes = $node->get_child_nodes();
+ $name = strtoupper(
+ $this->unquote_sqlite_identifier( $this->translate( $nodes[0] ) )
+ );
+
+ $args = array();
+ foreach ( $nodes[1]->get_child_nodes() as $child ) {
+ $args[] = $this->translate( $child );
+ }
+
+ switch ( $name ) {
+ case 'DATE_FORMAT':
+ list ( $date, $mysql_format ) = $args;
+
+ $format = strtr( $mysql_format, self::DATE_FORMAT_TO_STRFTIME_MAP );
+ if ( ! $format ) {
+ throw new Exception( "Could not translate a DATE_FORMAT() format to STRFTIME format ($mysql_format)" );
+ }
+
+ /*
+ * MySQL supports comparing strings and floats, e.g.
+ *
+ * > SELECT '00.42' = 0.4200
+ * 1
+ *
+ * SQLite does not support that. At the same time,
+ * WordPress likes to filter dates by comparing numeric
+ * outputs of DATE_FORMAT() to floats, e.g.:
+ *
+ * -- Filter by hour and minutes
+ * DATE_FORMAT(
+ * STR_TO_DATE('2014-10-21 00:42:29', '%Y-%m-%d %H:%i:%s'),
+ * '%H.%i'
+ * ) = 0.4200;
+ *
+ * Let's cast the STRFTIME() output to a float if
+ * the date format is typically used for string
+ * to float comparisons.
+ *
+ * In the future, let's update WordPress to avoid comparing
+ * strings and floats.
+ */
+ $cast_to_float = "'%H.%i'" === $mysql_format;
+ if ( true === $cast_to_float ) {
+ return sprintf( 'CAST(STRFTIME(%s, %s) AS FLOAT)', $format, $date );
+ }
+ return sprintf( 'STRFTIME(%s, %s)', $format, $date );
+ default:
+ return $this->translate_sequence( $node->get_children() );
+ }
+ }
+
private function get_sqlite_create_table_statement( string $table_name, ?string $new_table_name = null ): array {
// 1. Get table info.
$table_info = $this->execute_sqlite_query(
From f1c89ecdd4ae3b468f47c9178a91de4dd4799cd8 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 15 Jan 2025 09:24:25 +0100
Subject: [PATCH 038/124] Implement SQL_CALC_FOUND_ROWS and FOUND_ROWS()
---
tests/WP_SQLite_Driver_Tests.php | 26 ++++-
.../sqlite-ast/class-wp-sqlite-driver.php | 101 ++++++++++++++++--
2 files changed, 117 insertions(+), 10 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index e43a159..3cabb02 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -2819,17 +2819,37 @@ public function testCalcFoundRows() {
$this->assertNotFalse( $result );
$result = $this->assertQuery(
- "INSERT INTO wptests_dummy (user_login) VALUES ('test');"
+ "INSERT INTO wptests_dummy (user_login) VALUES ('test1');"
);
$this->assertEquals( '', $this->engine->get_error_message() );
$this->assertEquals( 1, $result );
$result = $this->assertQuery(
- 'SELECT SQL_CALC_FOUND_ROWS * FROM wptests_dummy'
+ "INSERT INTO wptests_dummy (user_login) VALUES ('test2');"
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result );
+
+ $result = $this->assertQuery(
+ 'SELECT SQL_CALC_FOUND_ROWS * FROM wptests_dummy ORDER BY ID LIMIT 1'
);
$this->assertNotFalse( $result );
+ $this->assertCount( 1, $result );
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 'test', $result[0]->user_login );
+ $this->assertEquals( 'test1', $result[0]->user_login );
+
+ $result = $this->assertQuery(
+ 'SELECT FOUND_ROWS()'
+ );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'FOUND_ROWS()' => '2',
+ ),
+ ),
+ $result
+ );
}
public function testComplexSelectBasedOnDates() {
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 3ebb3c4..b933d66 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -312,6 +312,20 @@ class WP_SQLite_Driver {
*/
private $last_insert_id;
+ /**
+ * Number of rows found by the last SELECT query.
+ *
+ * @var int
+ */
+ private $last_select_found_rows;
+
+ /**
+ * Number of rows found by the last SQL_CALC_FOUND_ROW query.
+ *
+ * @var int integer
+ */
+ private $last_sql_calc_found_rows = null;
+
/**
* Class variable to store the number of rows affected.
*
@@ -888,11 +902,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
switch ( $ast->rule_name ) {
case 'selectStatement':
$this->query_type = 'SELECT';
- $query = $this->translate( $ast->get_child() );
- $stmt = $this->execute_sqlite_query( $query );
- $this->set_results_from_fetched_data(
- $stmt->fetchAll( $this->pdo_fetch_mode )
- );
+ $this->execute_select_statement( $ast );
break;
case 'insertStatement':
case 'updateStatement':
@@ -984,6 +994,66 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
}
}
+ private function execute_select_statement( WP_Parser_Node $node ): void {
+ $has_sql_calc_found_rows = null !== $node->get_descendant_token(
+ WP_MySQL_Lexer::SQL_CALC_FOUND_ROWS_SYMBOL
+ );
+
+ // First, translate the query, before we modify last found rows count.
+ $query = $this->translate( $node->get_child() );
+
+ // Handle SQL_CALC_FOUND_ROWS.
+ if ( true === $has_sql_calc_found_rows ) {
+ // Recursively find a query expression with the first LIMIT or SELECT.
+ $query_expr = $node->get_descendant_node( 'queryExpression' );
+ while ( true ) {
+ if ( $query_expr->has_child_node( 'limitClause' ) ) {
+ break;
+ }
+
+ $query_expr_parens = $query_expr->get_child_node( 'queryExpressionParens' );
+ if ( null !== $query_expr_parens ) {
+ $query_expr = $query_expr_parens->get_child_node( 'queryExpression' );
+ continue;
+ }
+
+ $query_expr_body = $query_expr->get_child_node( 'queryExpressionBody' );
+ if ( count( $query_expr_body->get_children() ) > 1 ) {
+ break;
+ }
+
+ $query_term = $query_expr_body->get_child_node( 'queryTerm' );
+ if (
+ count( $query_term->get_children() ) === 1
+ && $query_term->has_child_node( 'queryExpressionParens' )
+ ) {
+ $query_expr = $query_term->get_child_node( 'queryExpressionParens' )->get_child_node( 'queryExpression' );
+ continue;
+ }
+
+ break;
+ }
+
+ $count_expr = new WP_Parser_Node( $query_expr->rule_id, $query_expr->rule_name );
+ foreach ( $query_expr->get_children() as $child ) {
+ if ( ! ( $child instanceof WP_Parser_Node && 'limitClause' === $child->rule_name ) ) {
+ $count_expr->append_child( $child );
+ }
+ }
+
+ $result = $this->execute_sqlite_query( 'SELECT COUNT(*) AS cnt FROM (' . $this->translate( $count_expr ) . ')' );
+ $this->last_sql_calc_found_rows = $result->fetchColumn();
+ } else {
+ $this->last_sql_calc_found_rows = null;
+ }
+
+ // Execute the query.
+ $stmt = $this->execute_sqlite_query( $query );
+ $this->set_results_from_fetched_data(
+ $stmt->fetchAll( $this->pdo_fetch_mode )
+ );
+ }
+
private function execute_update_statement( WP_Parser_Node $node ): void {
// @TODO: Add support for UPDATE with multiple tables and JOINs.
// SQLite supports them in the FROM clause.
@@ -1410,6 +1480,12 @@ private function translate_token( WP_MySQL_Token $token ) {
* and then remove it from the translated output here.
*/
return null;
+ case WP_MySQL_Lexer::SQL_CALC_FOUND_ROWS_SYMBOL:
+ /*
+ * The "SQL_CALC_FOUND_ROWS" keyword is implemented in the select
+ * statement translation and then removed from the output here.
+ */
+ return null;
default:
return $token->value;
}
@@ -1532,8 +1608,10 @@ private function translate_function_call( WP_Parser_Node $node ): string {
);
$args = array();
- foreach ( $nodes[1]->get_child_nodes() as $child ) {
- $args[] = $this->translate( $child );
+ if ( isset( $nodes[1] ) ) {
+ foreach ( $nodes[1]->get_child_nodes() as $child ) {
+ $args[] = $this->translate( $child );
+ }
}
switch ( $name ) {
@@ -1573,6 +1651,15 @@ private function translate_function_call( WP_Parser_Node $node ): string {
return sprintf( 'CAST(STRFTIME(%s, %s) AS FLOAT)', $format, $date );
}
return sprintf( 'STRFTIME(%s, %s)', $format, $date );
+ case 'FOUND_ROWS':
+ // @TODO: The following implementation with an alias assumes
+ // that the function is used in the SELECT field list.
+ // For compatibility with more complex use cases, it may
+ // be better to register it as a custom SQLite function.
+ return sprintf(
+ "(SELECT %d) AS 'FOUND_ROWS()'",
+ $this->last_sql_calc_found_rows ?? $this->last_select_found_rows
+ );
default:
return $this->translate_sequence( $node->get_children() );
}
From d3560216634a8a261f05551f89b9ccdd3ff668d5 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 15 Jan 2025 09:38:33 +0100
Subject: [PATCH 039/124] Implement FROM DUAL
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index b933d66..abe86d9 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1428,6 +1428,13 @@ private function translate( $ast ) {
throw $this->not_supported_exception(
sprintf( 'data type: %s', $child->value )
);
+ case 'fromClause':
+ // FROM DUAL is MySQL-specific syntax that means "FROM no tables"
+ // and it is equivalent to omitting the FROM clause entirely.
+ if ( $ast->has_child_token( WP_MySQL_Lexer::DUAL_SYMBOL ) ) {
+ return null;
+ }
+ return $this->translate_sequence( $ast->get_children() );
case 'predicateOperations':
$token = $ast->get_child_token();
if ( WP_MySQL_Lexer::LIKE_SYMBOL === $token->id ) {
From 8432d2a3632eaeb72494e9da09ae422e6ab57963 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 15 Jan 2025 09:52:09 +0100
Subject: [PATCH 040/124] Implement "CAST(... AS BINARY)"
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index abe86d9..5ea8c1c 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1454,6 +1454,12 @@ private function translate( $ast ) {
// When we have no value, it's reasonable to use NULL.
return 'NULL';
+ case 'castType':
+ // Translate "CAST(... AS BINARY)" to "CAST(... AS BLOB)".
+ if ( $ast->has_child_token( WP_MySQL_Lexer::BINARY_SYMBOL ) ) {
+ return 'BLOB';
+ }
+ return $this->translate_sequence( $ast->get_children() );
case 'defaultCollation':
// @TODO: Check and save in information schema.
return null;
From ab1e94f036aad9b8df5820c299cea7c2258fb91b Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 15 Jan 2025 10:31:47 +0100
Subject: [PATCH 041/124] Add support for CURRENT_TIMESTAMP() and NOW()
---
tests/WP_SQLite_Driver_Tests.php | 2 +-
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 8 ++++++++
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 3cabb02..4a040ae 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -3404,7 +3404,7 @@ public function testCurrentTimestamp() {
$this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t );
// UPDATE
- $this->assertQuery( 'UPDATE _dates SET option_value = NULL' );
+ $this->assertQuery( "UPDATE _dates SET option_value = ''" );
$results = $this->assertQuery( 'SELECT option_value AS t FROM _dates' );
$this->assertCount( 1, $results );
$this->assertEmpty( $results[0]->t );
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 5ea8c1c..3e04ae1 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1586,6 +1586,14 @@ private function translate_runtime_function_call( WP_Parser_Node $node ): string
}
switch ( $child->id ) {
+ case WP_MySQL_Lexer::CURRENT_TIMESTAMP_SYMBOL:
+ case WP_MySQL_Lexer::NOW_SYMBOL:
+ /*
+ * 1) SQLite doesn't support CURRENT_TIMESTAMP() with parentheses.
+ * 2) In MySQL, CURRENT_TIMESTAMP and CURRENT_TIMESTAMP() are an
+ * alias of NOW(). In SQLite, there is no NOW() function.
+ */
+ return 'CURRENT_TIMESTAMP';
case WP_MySQL_Lexer::DATE_ADD_SYMBOL:
case WP_MySQL_Lexer::DATE_SUB_SYMBOL:
$nodes = $node->get_child_nodes();
From 6f2ffb01bc75770f328000d47af55d4a6368689a Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 15 Jan 2025 15:03:30 +0100
Subject: [PATCH 042/124] Translate datetime literals, fix string literal
processing
---
.../sqlite-ast/class-wp-sqlite-driver.php | 85 +++++++++++++++++--
1 file changed, 79 insertions(+), 6 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 3e04ae1..23afb9a 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1387,13 +1387,27 @@ private function translate( $ast ) {
return '"' . $this->translate( $ast->get_child() ) . '"';
case 'textStringLiteral':
$token = $ast->get_child_token();
- if ( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT === $token->id ) {
- return WP_SQLite_Token_Factory::double_quoted_value( $token->value )->value;
- }
- if ( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT === $token->id ) {
- return WP_SQLite_Token_Factory::raw( $token->value )->value;
+
+ // 1. Remove bounding quotes.
+ $quote = $token->value[0];
+ $value = substr( $token->value, 1, -1 );
+
+ // 2. Unescape quotes within the string.
+ $value = str_replace( $quote . $quote, $quote, $value );
+ $value = str_replace( '\\' . $quote, $quote, $value );
+
+ // 3. Translate datetime literals.
+ // Process only strings that could possibly represent a datetime
+ // literal ("YYYY-MM-DDTHH:MM:SS", "YYYY-MM-DDTHH:MM:SSZ", etc.).
+ if ( strlen( $value ) >= 19 && is_numeric( $value[0] ) ) {
+ $value = $this->translate_datetime_literal( $value );
}
- throw $this->invalid_input_exception();
+
+ // 4. Remove null characters.
+ $value = str_replace( "\0", '', $value );
+
+ // 5. Escape and add quotes.
+ return "'" . str_replace( "'", "''", $value ) . "'";
case 'dataType':
case 'nchar':
$child = $ast->get_child();
@@ -1686,6 +1700,65 @@ private function translate_function_call( WP_Parser_Node $node ): string {
}
}
+ private function translate_datetime_literal( string $value ): string {
+ /*
+ * The code below converts the date format to one preferred by SQLite.
+ *
+ * MySQL accepts ISO 8601 date strings: 'YYYY-MM-DDTHH:MM:SSZ'
+ * SQLite prefers a slightly different format: 'YYYY-MM-DD HH:MM:SS'
+ *
+ * SQLite date and time functions can understand the ISO 8601 notation, but
+ * lookups don't. To keep the lookups working, we need to store all dates
+ * in UTC without the "T" and "Z" characters.
+ *
+ * Caveat: It will adjust every string that matches the pattern, not just dates.
+ *
+ * In theory, we could only adjust semantic dates, e.g. the data inserted
+ * to a date column or compared against a date column.
+ *
+ * In practice, this is hard because dates are just text – SQLite has no separate
+ * datetime field. We'd need to cache the MySQL data type from the original
+ * CREATE TABLE query and then keep refreshing the cache after each ALTER TABLE query.
+ *
+ * That's a lot of complexity that's perhaps not worth it. Let's just convert
+ * everything for now. The regexp assumes "Z" is always at the end of the string,
+ * which is true in the unit test suite, but there could also be a timezone offset
+ * like "+00:00" or "+01:00". We could add support for that later if needed.
+ */
+ if ( 1 === preg_match( '/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})Z$/', $value, $matches ) ) {
+ $value = $matches[1] . ' ' . $matches[2];
+ }
+
+ /*
+ * Mimic MySQL's behavior and truncate invalid dates.
+ *
+ * "2020-12-41 14:15:27" becomes "0000-00-00 00:00:00"
+ *
+ * WARNING: We have no idea whether the truncated value should
+ * be treated as a date in the first place.
+ * In SQLite dates are just strings. This could be a perfectly
+ * valid string that just happens to contain a date-like value.
+ *
+ * At the same time, WordPress seems to rely on MySQL's behavior
+ * and even tests for it in Tests_Post_wpInsertPost::test_insert_empty_post_date.
+ * Let's truncate the dates for now.
+ *
+ * In the future, let's update WordPress to do its own date validation
+ * and stop relying on this MySQL feature,
+ */
+ if ( 1 === preg_match( '/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/', $value, $matches ) ) {
+ /*
+ * Calling strtotime("0000-00-00 00:00:00") in 32-bit environments triggers
+ * an "out of integer range" warning – let's avoid that call for the popular
+ * case of "zero" dates.
+ */
+ if ( '0000-00-00 00:00:00' !== $value && false === strtotime( $value ) ) {
+ $value = '0000-00-00 00:00:00';
+ }
+ }
+ return $value;
+ }
+
private function get_sqlite_create_table_statement( string $table_name, ?string $new_table_name = null ): array {
// 1. Get table info.
$table_info = $this->execute_sqlite_query(
From 6933c721ef5b5c295ccffa40c90f65f3762fb810 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 15 Jan 2025 15:23:24 +0100
Subject: [PATCH 043/124] Add support for HAVING without GROUP BY
---
tests/WP_SQLite_Driver_Tests.php | 2 +-
.../sqlite-ast/class-wp-sqlite-driver.php | 16 ++++++++++++++++
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 4a040ae..20bee52 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -3467,7 +3467,7 @@ public function testHavingWithoutGroupBy() {
$this->assertEquals(
array(
(object) array(
- ':param0' => 'T',
+ "'T'" => 'T',
),
),
$result
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 23afb9a..9086d71 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1380,6 +1380,22 @@ private function translate( $ast ) {
$rule_name = $ast->rule_name;
switch ( $rule_name ) {
+ case 'querySpecification':
+ // Translate "HAVING ..." without "GROUP BY ..." to "GROUP BY 1 HAVING ...".
+ if ( $ast->has_child_node( 'havingClause' ) && ! $ast->has_child_node( 'groupByClause' ) ) {
+ $parts = array();
+ foreach ( $ast->get_children() as $child ) {
+ if ( $child instanceof WP_Parser_Node && 'havingClause' === $child->rule_name ) {
+ $parts[] = 'GROUP BY 1';
+ }
+ $part = $this->translate( $child );
+ if ( null !== $part ) {
+ $parts[] = $part;
+ }
+ }
+ return implode( ' ', $parts );
+ }
+ return $this->translate_sequence( $ast->get_children() );
case 'qualifiedIdentifier':
case 'dotIdentifier':
return $this->translate_sequence( $ast->get_children(), '' );
From 32c72bfc3fdf35c5509f228300e0fdc16e9e39fb Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 16 Jan 2025 09:54:52 +0100
Subject: [PATCH 044/124] Implement DROP for multiple and temporary tables
---
.../sqlite-ast/class-wp-sqlite-driver.php | 56 +++++++++++++++++--
1 file changed, 52 insertions(+), 4 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 9086d71..6572b6b 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -957,9 +957,16 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
break;
case 'dropStatement':
$this->query_type = 'DROP';
- $query = $this->translate( $ast );
- $this->execute_sqlite_query( $query );
- $this->set_result_from_affected_rows();
+ $subtree = $ast->get_child_node();
+ switch ( $subtree->rule_name ) {
+ case 'dropTable':
+ $this->execute_drop_table_statement( $ast );
+ break;
+ default:
+ $query = $this->translate( $ast );
+ $this->execute_sqlite_query( $query );
+ $this->set_result_from_affected_rows();
+ }
break;
case 'setStatement':
/*
@@ -1101,8 +1108,9 @@ private function execute_update_statement( WP_Parser_Node $node ): void {
}
private function execute_create_table_statement( WP_Parser_Node $node ): void {
+ $is_temporary = $node->get_child_node()->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL );
$element_list = $node->get_descendant_node( 'tableElementList' );
- if ( null === $element_list ) {
+ if ( true === $is_temporary || null === $element_list ) {
$query = $this->translate( $node );
$this->execute_sqlite_query( $query );
$this->set_result_from_affected_rows();
@@ -1263,6 +1271,46 @@ function ( $column ) {
*/
}
+ private function execute_drop_table_statement( WP_Parser_Node $node ): void {
+ $child_node = $node->get_child_node();
+
+ // MySQL supports removing multiple tables in a single query DROP query.
+ // In SQLite, we need to execute each DROP TABLE statement separately.
+ $table_refs = $child_node->get_child_node( 'tableRefList' )->get_child_nodes();
+ $is_temporary = $child_node->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL );
+ $queries = array();
+ foreach ( $table_refs as $table_ref ) {
+ $parts = array();
+ foreach ( $child_node->get_children() as $child ) {
+ $is_token = $child instanceof WP_MySQL_Token;
+
+ // Skip the TEMPORARY keyword.
+ if ( $is_token && WP_MySQL_Lexer::TEMPORARY_SYMBOL === $child->id ) {
+ continue;
+ }
+
+ // Replace table list with the current table reference.
+ if ( ! $is_token && 'tableRefList' === $child->rule_name ) {
+ // Add a "temp." schema prefix for temporary tables.
+ $prefix = $is_temporary ? '"temp".' : '';
+ $part = $prefix . $this->translate( $table_ref );
+ } else {
+ $part = $this->translate( $child );
+ }
+
+ if ( null !== $part ) {
+ $parts[] = $part;
+ }
+ }
+ $queries[] = 'DROP ' . implode( ' ', $parts );
+ }
+
+ foreach ( $queries as $query ) {
+ $this->execute_sqlite_query( $query );
+ }
+ $this->results = $this->last_exec_returned;
+ }
+
private function execute_show_statement( WP_Parser_Node $node ): void {
$tokens = $node->get_child_tokens();
$keyword1 = $tokens[1];
From 822464a117c7a5c7a83d689e728f4cfdd2c18843 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 16 Jan 2025 11:25:13 +0100
Subject: [PATCH 045/124] Implement ON DUPLICATE KEY UPDATE
---
grammar-tools/MySQLParser.g4 | 12 ++--
tests/WP_SQLite_Driver_Tests.php | 72 +++++++++----------
wp-includes/mysql/mysql-grammar.php | 2 +-
.../sqlite-ast/class-wp-sqlite-driver.php | 29 ++++++++
4 files changed, 74 insertions(+), 41 deletions(-)
diff --git a/grammar-tools/MySQLParser.g4 b/grammar-tools/MySQLParser.g4
index b8fc831..efdc45b 100644
--- a/grammar-tools/MySQLParser.g4
+++ b/grammar-tools/MySQLParser.g4
@@ -2965,9 +2965,9 @@ simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*;
literal # simpleExprLiteral
| sumExpr # simpleExprSum
| variable (equal expr)? # simpleExprVariable
- | functionCall # simpleExprFunction
- | runtimeFunctionCall # simpleExprRuntimeFunction
- | columnRef jsonOperator? # simpleExprColumnRef
+ /*| functionCall # simpleExprFunction*/
+ /*| runtimeFunctionCall # simpleExprRuntimeFunction*/
+ /*| columnRef jsonOperator? # simpleExprColumnRef*/
| PARAM_MARKER # simpleExprParamMarker
| {serverVersion >= 80000}? groupingOperation # simpleExprGroupingOperation
| {serverVersion >= 80000}? windowFunctionCall # simpleExprWindowingFunction
@@ -2991,6 +2991,10 @@ simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*;
| DEFAULT_SYMBOL OPEN_PAR_SYMBOL simpleIdentifier CLOSE_PAR_SYMBOL # simpleExprDefault
| VALUES_SYMBOL OPEN_PAR_SYMBOL simpleIdentifier CLOSE_PAR_SYMBOL # simpleExprValues
| INTERVAL_SYMBOL expr interval PLUS_OPERATOR expr # simpleExprInterval
+ /* @FIX: Move function calls and ref to the end to avoid conflicts with the above expressions. */
+ | functionCall # simpleExprFunction
+ | runtimeFunctionCall # simpleExprRuntimeFunction
+ | columnRef jsonOperator? # simpleExprColumnRef
;
arrayCast:
@@ -3175,7 +3179,7 @@ runtimeFunctionCall:
| name = WEEK_SYMBOL OPEN_PAR_SYMBOL expr (COMMA_SYMBOL expr)? CLOSE_PAR_SYMBOL
| name = WEIGHT_STRING_SYMBOL OPEN_PAR_SYMBOL expr AS_SYMBOL CHAR_SYMBOL CLOSE_PAR_SYMBOL
| name = WEIGHT_STRING_SYMBOL OPEN_PAR_SYMBOL expr (
- /* @FIX: Move "AS BINARY(...)" before "AS CHAR(...)" to solve conflict. */
+ /* @FIX: Move "AS BINARY(...)" before "AS CHAR(...)" to solve conflict. */
AS_SYMBOL BINARY_SYMBOL wsNumCodepoints
| (AS_SYMBOL CHAR_SYMBOL wsNumCodepoints)? (
{serverVersion < 80000}? weightStringLevels
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 20bee52..c33d3b8 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -2062,9 +2062,9 @@ public function testOnDuplicateUpdate() {
);"
);
- // $result1 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" );
- // $this->assertEquals( '', $this->engine->get_error_message() );
- // $this->assertEquals( 1, $result1 );
+ $result1 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" );
+ $this->assertEquals( '', $this->engine->get_error_message() );
+ $this->assertEquals( 1, $result1 );
$result2 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST') ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);" );
$this->assertEquals( 1, $result2 );
@@ -2452,7 +2452,7 @@ public function testInsertOnDuplicateKey() {
$result1 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" );
$this->assertEquals( 1, $result1 );
- $result2 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST') ON DUPLICATE KEY SET name=VALUES(`name`);" );
+ $result2 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST') ON DUPLICATE KEY UPDATE name=VALUES(`name`);" );
$this->assertEquals( 1, $result2 );
$this->assertQuery( 'SELECT COUNT(*) as cnt FROM _tmp_table' );
@@ -2775,7 +2775,7 @@ public function testInsertOnDuplicateKeyCompositePk() {
$this->assertEquals( '', $this->engine->get_error_message() );
$this->assertEquals( 2, $result1 );
- $result2 = $this->assertQuery( 'INSERT INTO wptests_term_relationships VALUES (1,2,2),(1,3,1) ON DUPLICATE KEY SET term_order = VALUES(term_order);' );
+ $result2 = $this->assertQuery( 'INSERT INTO wptests_term_relationships VALUES (1,2,2),(1,3,1) ON DUPLICATE KEY UPDATE term_order = VALUES(term_order);' );
$this->assertEquals( '', $this->engine->get_error_message() );
$this->assertEquals( 2, $result2 );
@@ -3267,21 +3267,20 @@ public function testTranslateLikeBinary() {
$this->assertCount( 2, $result ); // Should match both 'first' and 'FIRST'
}
- public function testOnConflictReplace() {
+ public function testUniqueConstraints() {
$this->assertQuery(
"CREATE TABLE _tmp_table (
ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
name varchar(20) NOT NULL default 'default-value',
unique_name varchar(20) NOT NULL default 'unique-default-value',
- inline_unique_name varchar(20) NOT NULL default 'inline-unique-default-value',
- no_default varchar(20) NOT NULL,
- UNIQUE KEY unique_name (unique_name)
+ inline_unique_name varchar(30) NOT NULL default 'inline-unique-default-value' UNIQUE,
+ UNIQUE KEY unique_name (unique_name),
+ UNIQUE KEY compound_name (name, unique_name)
);"
);
- $this->assertQuery(
- "INSERT INTO _tmp_table VALUES (1, null, null, null, '');"
- );
+ // Insert a row with default values.
+ $this->assertQuery( 'INSERT INTO _tmp_table (ID) VALUES (1)' );
$result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE ID = 1' );
$this->assertEquals(
array(
@@ -3290,45 +3289,47 @@ public function testOnConflictReplace() {
'name' => 'default-value',
'unique_name' => 'unique-default-value',
'inline_unique_name' => 'inline-unique-default-value',
- 'no_default' => '',
),
),
$result
);
+ // Insert another row.
+ $this->assertQuery(
+ "INSERT INTO _tmp_table VALUES (2, 'ANOTHER-VALUE', 'ANOTHER-UNIQUE-VALUE', 'ANOTHER-INLINE-UNIQUE-VALUE')"
+ );
+
+ // This should fail because of the UNIQUE constraints.
$this->assertQuery(
- "INSERT INTO _tmp_table VALUES (2, '1', '2', '3', '4');"
+ "UPDATE _tmp_table SET unique_name = 'unique-default-value' WHERE ID = 2",
+ 'UNIQUE constraint failed: _tmp_table.unique_name'
);
+
$this->assertQuery(
- 'UPDATE _tmp_table SET name = null WHERE ID = 2;'
+ "UPDATE _tmp_table SET inline_unique_name = 'inline-unique-default-value' WHERE ID = 2",
+ 'UNIQUE constraint failed: _tmp_table.inline_unique_name'
);
- $result = $this->assertQuery( 'SELECT name FROM _tmp_table WHERE ID = 2' );
+ // Updating "name" to the same value as the first row should pass.
+ $this->assertQuery(
+ "UPDATE _tmp_table SET name = 'default-value' WHERE ID = 2"
+ );
$this->assertEquals(
array(
(object) array(
- 'name' => 'default-value',
+ 'ID' => '2',
+ 'name' => 'default-value',
+ 'unique_name' => 'ANOTHER-UNIQUE-VALUE',
+ 'inline_unique_name' => 'ANOTHER-INLINE-UNIQUE-VALUE',
),
),
- $result
+ $this->assertQuery( 'SELECT * FROM _tmp_table WHERE ID = 2' )
);
- // This should fail because of the UNIQUE constraint
+ // Updating also "unique_name" should fail on the compound UNIQUE key.
$this->assertQuery(
- 'UPDATE _tmp_table SET unique_name = NULL WHERE ID = 2;',
- 'UNIQUE constraint failed: _tmp_table.unique_name'
- );
-
- // Inline unique constraint aren't supported currently, so this should pass
- $this->assertQuery(
- 'UPDATE _tmp_table SET inline_unique_name = NULL WHERE ID = 2;',
- ''
- );
-
- // WPDB allows for NULL values in columns that don't have a default value and a NOT NULL constraint
- $this->assertQuery(
- 'UPDATE _tmp_table SET no_default = NULL WHERE ID = 2;',
- ''
+ "UPDATE _tmp_table SET inline_unique_name = 'inline-unique-default-value' WHERE ID = 2",
+ 'UNIQUE constraint failed: _tmp_table.inline_unique_name'
);
$result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE ID = 2' );
@@ -3337,9 +3338,8 @@ public function testOnConflictReplace() {
(object) array(
'ID' => '2',
'name' => 'default-value',
- 'unique_name' => '2',
- 'inline_unique_name' => 'inline-unique-default-value',
- 'no_default' => '',
+ 'unique_name' => 'ANOTHER-UNIQUE-VALUE',
+ 'inline_unique_name' => 'ANOTHER-INLINE-UNIQUE-VALUE',
),
),
$result
diff --git a/wp-includes/mysql/mysql-grammar.php b/wp-includes/mysql/mysql-grammar.php
index 8c0bc7a..3aec6f4 100644
--- a/wp-includes/mysql/mysql-grammar.php
+++ b/wp-includes/mysql/mysql-grammar.php
@@ -1,4 +1,4 @@
2000,'rules_names'=>['query','%f1','%f2','%f3','simpleStatement','%f4','%f5','%f6','%f7','alterStatement','%f8','%f9','%f10','%f11','alterInstance','%f12','%f13','%f14','%f15','%f16','%f17','%f18','%f19','%f20','%f21','%f22','%f23','%f24','%f25','%f26','%f27','alterDatabase','%f28','%f29','%f30','%f31','%f32','%f33','%f34','%f35','%f36','%f37','alterEvent','%f38','%f39','%f40','%f41','%f42','%f43','%f44','%f45','%f46','%f47','%f48','%f49','%f50','%f51','%f52','%f53','%f54','alterLogfileGroup','%f55','alterLogfileGroupOptions','%f56','%f57','%f58','alterLogfileGroupOption','alterServer','%f59','%f60','%f61','alterTable','%f62','%f63','%f64','%f65','%f66','alterTableActions','%f67','%f68','%f69','%f70','%f71','alterCommandList','%f72','%f73','%f74','alterCommandsModifierList','%f75','%f76','standaloneAlterCommands','%f77','%f78','%f79','%f80','%f81','%f82','%f83','%f84','%f85','%f86','%f87','%f88','%f89','%f90','alterPartition','%f91','%f92','%f93','%f94','%f95','%f96','alterList','%f97','%f98','%f99','%f100','alterCommandsModifier','%f101','%f102','%f103','%f104','%f105','%f106','%f107','%f108','alterListItem','%f109','%f110','%f111','%f112','%f113','%f114','%f115','%f116','%f117','%f118','%f119','%f120','%f121','%f122','%f123','%f124','%f125','%f126','%f127','%f128','%f129','%f130','%f131','%f132','%f133','%f134','place','restrict','%f135','%f136','alterOrderList','%f137','%f138','%f139','%f140','alterAlgorithmOption','%f141','%f142','alterLockOption','%f143','%f144','%f145','indexLockAndAlgorithm','withValidation','%f146','%f147','removePartitioning','allOrPartitionNameList','alterTablespace','%f148','%f149','%f150','%f151','%f152','%f153','%f154','%f155','%f156','%f157','%f158','%f159','%f160','%f161','%f162','alterUndoTablespace','%f163','%f164','undoTableSpaceOptions','%f165','%f166','%f167','undoTableSpaceOption','%f168','alterTablespaceOptions','%f169','%f170','%f171','%f172','alterTablespaceOption','%f173','%f174','changeTablespaceOption','%f175','%f176','%f177','alterView','%f178','viewTail','%f179','viewSelect','%f180','viewCheckOption','%f181','%f182','createStatement','%f183','%f184','%f185','%f186','%f187','%f188','createDatabase','createDatabaseOption','%f189','%f190','%f191','createTable','%f192','%f193','%f194','%f195','%f196','%f197','%f198','%f199','tableElementList','%f200','%f201','tableElement','%f202','%f203','duplicateAsQueryExpression','%f204','%f205','queryExpressionOrParens','%f206','createRoutine','%f207','%f208','%f209','%f210','createProcedure','%f211','%f212','%f213','%f214','%f215','%f216','%f217','%f218','%f219','%f220','%f221','createFunction','%f222','%f223','%f224','%f225','%f226','%f227','%f228','%f229','%f230','createUdf','%f231','%f232','routineCreateOption','%f233','routineAlterOptions','routineOption','%f234','%f235','%f236','createIndex','%f237','%f238','%f239','%f240','%f241','%f242','%f243','%f244','%f245','%f246','%f247','indexNameAndType','%f248','%f249','%f250','createIndexTarget','%f251','createLogfileGroup','%f252','%f253','logfileGroupOptions','%f254','%f255','%f256','logfileGroupOption','createServer','%f257','serverOptions','%f258','%f259','serverOption','%f260','%f261','createTablespace','%f262','%f263','%f264','createUndoTablespace','tsDataFileName','%f265','%f266','%f267','%f268','tsDataFile','%f269','tablespaceOptions','%f270','%f271','%f272','tablespaceOption','%f273','%f274','%f275','%f276','tsOptionInitialSize','%f277','tsOptionUndoRedoBufferSize','%f278','%f279','tsOptionAutoextendSize','%f280','tsOptionMaxSize','%f281','tsOptionExtentSize','%f282','tsOptionNodegroup','%f283','%f284','tsOptionEngine','%f285','tsOptionEngineAttribute','tsOptionWait','%f286','%f287','tsOptionComment','%f288','tsOptionFileblockSize','%f289','tsOptionEncryption','%f290','%f291','%f292','createView','%f293','viewReplaceOrAlgorithm','viewAlgorithm','%f294','viewSuid','%f295','%f296','%f297','createTrigger','%f298','%f299','%f300','%f301','%f302','triggerFollowsPrecedesClause','%f303','%f304','%f305','%f306','%f307','%f308','%f309','createEvent','%f310','%f311','%f312','%f313','%f314','%f315','%f316','%f317','%f318','%f319','%f320','createRole','%f321','%f322','%f323','createSpatialReference','srsAttribute','dropStatement','%f324','%f325','%f326','%f327','%f328','dropDatabase','%f329','dropEvent','%f330','dropFunction','%f331','dropProcedure','%f332','%f333','dropIndex','%f334','dropLogfileGroup','%f335','%f336','%f337','%f338','%f339','%f340','dropLogfileGroupOption','%f341','dropServer','%f342','%f343','%f344','dropTable','%f345','%f346','%f347','%f348','dropTableSpace','%f349','%f350','%f351','%f352','%f353','%f354','%f355','dropTrigger','%f356','%f357','dropView','%f358','%f359','%f360','dropRole','%f361','dropSpatialReference','%f362','dropUndoTablespace','%f363','renameTableStatement','%f364','%f365','%f366','renamePair','%f367','truncateTableStatement','importStatement','%f368','callStatement','%f369','%f370','%f371','%f372','%f373','deleteStatement','%f374','%f375','%f376','%f377','%f378','%f379','%f380','%f381','%f382','%f383','%f384','%f385','%f386','%f387','%f388','partitionDelete','%f389','deleteStatementOption','doStatement','%f390','%f391','%f392','handlerStatement','%f393','%f394','%f395','%f396','%f397','handlerReadOrScan','%f398','%f399','%f400','%f401','%f402','%f403','%f404','%f405','%f406','insertStatement','%f407','%f408','%f409','%f410','%f411','%f412','%f413','%f414','%f415','insertLockOption','%f416','insertFromConstructor','%f417','%f418','%f419','%f420','fields','%f421','%f422','insertValues','%f423','%f424','insertQueryExpression','%f425','%f426','valueList','%f427','%f428','%f429','%f430','values','%f431','%f432','%f433','%f434','%f435','valuesReference','insertUpdateList','%f436','%f437','%f438','%f439','%f440','%f441','%f442','%f443','loadStatement','%f444','%f445','%f446','%f447','dataOrXml','xmlRowsIdentifiedBy','%f448','%f449','%f450','loadDataFileTail','%f451','%f452','%f453','%f454','%f455','%f456','loadDataFileTargetList','%f457','fieldOrVariableList','%f458','%f459','%f460','%f461','%f462','%f463','%f464','replaceStatement','%f465','%f466','%f467','%f468','selectStatement','%f469','selectStatementWithInto','%f470','%f471','queryExpression','%f472','%f473','%f474','%f475','%f476','%f477','%f478','%f479','%f480','%f481','%f482','%f483','queryExpressionBody','%f484','%f485','%f486','%f487','%f488','%f489','queryTerm','%f490','%f491','%f492','%f493','%f494','%f495','%f496','queryExpressionParens','queryPrimary','%f497','%f498','%f499','%f500','%f501','%f502','%f503','%f504','%f505','querySpecification','%f506','%f507','%f508','subquery','querySpecOption','limitClause','simpleLimitClause','%f509','limitOptions','%f510','%f511','%f512','limitOption','%f513','intoClause','%f514','%f515','%f516','%f517','%f518','%f519','%f520','%f521','%f522','%f523','procedureAnalyseClause','%f524','%f525','%f526','%f527','%f528','havingClause','%f529','windowClause','%f530','%f531','windowDefinition','windowSpec','%f532','%f533','%f534','%f535','%f536','%f537','%f538','%f539','%f540','%f541','windowSpecDetails','%f542','%f543','%f544','%f545','%f546','%f547','%f548','windowFrameClause','windowFrameUnits','windowFrameExtent','windowFrameStart','windowFrameBetween','windowFrameBound','windowFrameExclusion','%f549','%f550','%f551','withClause','%f552','%f553','%f554','commonTableExpression','%f555','groupByClause','olapOption','%f556','orderClause','direction','fromClause','%f557','%f558','tableReferenceList','%f559','%f560','%f561','tableValueConstructor','%f562','%f563','explicitTable','%f564','rowValueExplicit','selectOption','%f565','%f566','%f567','lockingClauseList','%f568','%f569','lockingClause','%f570','%f571','%f572','%f573','%f574','%f575','lockStrengh','%f576','lockedRowAction','%f577','selectItemList','%f578','%f579','%f580','%f581','selectItem','%f582','selectAlias','%f583','whereClause','%f584','tableReference','%f585','%f586','%f587','%f588','escapedTableReference','%f589','joinedTable','%f590','%f591','%f592','%f593','%f594','naturalJoinType','%f595','%f596','innerJoinType','%f597','%f598','%f599','outerJoinType','%f600','tableFactor','%f601','%f602','%f603','%f604','singleTable','singleTableParens','%f605','%f606','%f607','derivedTable','%f608','%f609','%f610','%f611','%f612','%f613','tableReferenceListParens','%f614','%f615','tableFunction','%f616','columnsClause','%f617','%f618','%f619','%f620','%f621','jtColumn','%f622','%f623','%f624','%f625','%f626','onEmptyOrError','onEmpty','onError','jtOnResponse','setOperationOption','%f627','tableAlias','%f628','%f629','%f630','%f631','indexHintList','%f632','%f633','%f634','indexHint','indexHintType','keyOrIndex','%f635','constraintKeyType','indexHintClause','%f636','%f637','indexList','%f638','%f639','indexListElement','%f640','%f641','%f642','%f643','%f644','%f645','updateStatement','%f646','%f647','%f648','transactionOrLockingStatement','%f649','%f650','%f651','%f652','transactionStatement','%f653','%f654','%f655','%f656','%f657','%f658','%f659','beginWork','%f660','transactionCharacteristicList','%f661','%f662','transactionCharacteristic','%f663','%f664','%f665','savepointStatement','%f666','%f667','%f668','%f669','%f670','%f671','%f672','%f673','%f674','%f675','%f676','lockStatement','%f677','%f678','%f679','%f680','%f681','%f682','%f683','lockItem','%f684','%f685','lockOption','xaStatement','%f686','%f687','%f688','%f689','%f690','%f691','%f692','%f693','%f694','%f695','%f696','%f697','%f698','%f699','xaConvert','%f700','%f701','%f702','%f703','%f704','xid','%f705','%f706','%f707','%f708','%f709','%f710','replicationStatement','%f711','%f712','%f713','%f714','%f715','%f716','%f717','%f718','%f719','%f720','%f721','%f722','%f723','%f724','resetOption','%f725','masterResetOptions','%f726','%f727','%f728','%f729','replicationLoad','%f730','%f731','changeMaster','%f732','changeMasterOptions','%f733','%f734','masterOption','privilegeCheckDef','tablePrimaryKeyCheckDef','masterTlsCiphersuitesDef','masterFileDef','%f735','serverIdList','%f736','%f737','%f738','%f739','%f740','%f741','%f742','%f743','changeReplication','%f744','%f745','%f746','%f747','%f748','%f749','changeReplicationSourceOptions','%f750','%f751','replicationSourceOption','%f752','%f753','%f754','%f755','%f756','%f757','%f758','%f759','%f760','%f761','%f762','%f763','%f764','%f765','%f766','%f767','%f768','%f769','%f770','%f771','%f772','%f773','%f774','%f775','%f776','%f777','%f778','%f779','%f780','%f781','%f782','%f783','%f784','%f785','filterDefinition','%f786','filterDbList','%f787','%f788','%f789','filterTableList','%f790','%f791','%f792','filterStringList','%f793','%f794','filterWildDbTableString','%f795','filterDbPairList','%f796','%f797','%f798','%f799','%f800','%f801','%f802','slave','%f803','%f804','%f805','slaveUntilOptions','%f806','%f807','%f808','%f809','%f810','%f811','slaveConnectionOptions','%f812','%f813','%f814','%f815','%f816','%f817','%f818','%f819','%f820','%f821','%f822','%f823','%f824','%f825','slaveThreadOptions','%f826','%f827','slaveThreadOption','groupReplication','%f828','preparedStatement','%f829','%f830','%f831','executeStatement','%f832','%f833','%f834','executeVarList','%f835','%f836','cloneStatement','%f837','%f838','%f839','%f840','%f841','%f842','%f843','%f844','%f845','dataDirSSL','%f846','ssl','accountManagementStatement','%f847','%f848','%f849','alterUser','%f850','%f851','%f852','alterUserTail','%f853','%f854','%f855','%f856','%f857','%f858','userFunction','createUser','%f859','%f860','createUserTail','%f861','%f862','%f863','%f864','%f865','%f866','%f867','%f868','%f869','defaultRoleClause','%f870','%f871','%f872','%f873','requireClause','%f874','%f875','%f876','connectOptions','%f877','%f878','accountLockPasswordExpireOptions','%f879','%f880','%f881','%f882','%f883','%f884','%f885','%f886','%f887','%f888','%f889','%f890','%f891','dropUser','%f892','%f893','%f894','grant','%f895','%f896','%f897','%f898','%f899','%f900','%f901','%f902','%f903','%f904','%f905','%f906','%f907','%f908','grantTargetList','%f909','%f910','grantOptions','%f911','%f912','%f913','exceptRoleList','withRoles','%f914','%f915','%f916','grantAs','versionedRequireClause','%f917','%f918','renameUser','%f919','%f920','%f921','%f922','revoke','%f923','%f924','%f925','%f926','%f927','%f928','%f929','%f930','%f931','%f932','%f933','%f934','%f935','%f936','%f937','%f938','onTypeTo','%f939','%f940','%f941','%f942','%f943','%f944','%f945','aclType','%f946','roleOrPrivilegesList','%f947','%f948','%f949','%f950','%f951','roleOrPrivilege','%f952','%f953','%f954','%f955','%f956','%f957','%f958','%f959','%f960','%f961','%f962','%f963','%f964','grantIdentifier','%f965','%f966','%f967','%f968','%f969','requireList','%f970','%f971','%f972','requireListElement','grantOption','%f973','setRole','%f974','%f975','%f976','%f977','%f978','roleList','%f979','%f980','%f981','role','%f982','%f983','%f984','%f985','%f986','%f987','%f988','%f989','%f990','tableAdministrationStatement','%f991','%f992','%f993','%f994','%f995','%f996','%f997','%f998','%f999','%f1000','%f1001','%f1002','histogram','%f1003','%f1004','%f1005','%f1006','checkOption','%f1007','repairType','%f1008','%f1009','installUninstallStatment','%f1010','%f1011','%f1012','%f1013','installOptionType','%f1014','installSetValue','%f1015','%f1016','installSetValueList','%f1017','%f1018','setStatement','%f1019','startOptionValueList','%f1020','%f1021','%f1022','%f1023','%f1024','%f1025','%f1026','%f1027','%f1028','%f1029','%f1030','%f1031','%f1032','%f1033','%f1034','%f1035','%f1036','transactionCharacteristics','%f1037','%f1038','%f1039','%f1040','transactionAccessMode','%f1041','isolationLevel','%f1042','%f1043','%f1044','optionValueListContinued','%f1045','%f1046','optionValueNoOptionType','%f1047','%f1048','%f1049','optionValue','%f1050','setSystemVariable','startOptionValueListFollowingOptionType','optionValueFollowingOptionType','setExprOrDefault','%f1051','%f1052','%f1053','showStatement','%f1054','%f1055','%f1056','%f1057','%f1058','%f1059','%f1060','%f1061','%f1062','%f1063','%f1064','%f1065','%f1066','%f1067','%f1068','%f1069','%f1070','%f1071','%f1072','%f1073','%f1074','%f1075','%f1076','%f1077','%f1078','%f1079','%f1080','%f1081','%f1082','%f1083','%f1084','%f1085','%f1086','%f1087','%f1088','%f1089','%f1090','%f1091','%f1092','%f1093','%f1094','%f1095','%f1096','%f1097','%f1098','%f1099','%f1100','%f1101','%f1102','%f1103','%f1104','%f1105','%f1106','%f1107','%f1108','%f1109','%f1110','%f1111','%f1112','%f1113','%f1114','%f1115','%f1116','%f1117','%f1118','%f1119','%f1120','%f1121','%f1122','showCommandType','%f1123','%f1124','nonBlocking','%f1125','%f1126','fromOrIn','inDb','profileType','%f1127','%f1128','%f1129','otherAdministrativeStatement','%f1130','%f1131','%f1132','%f1133','%f1134','%f1135','%f1136','%f1137','keyCacheListOrParts','%f1138','keyCacheList','%f1139','%f1140','%f1141','assignToKeycache','%f1142','assignToKeycachePartition','%f1143','cacheKeyList','keyUsageElement','%f1144','keyUsageList','%f1145','%f1146','%f1147','%f1148','flushOption','%f1149','%f1150','%f1151','logType','%f1152','flushTables','%f1153','%f1154','%f1155','%f1156','flushTablesOptions','%f1157','%f1158','%f1159','preloadTail','%f1160','%f1161','%f1162','preloadList','%f1163','%f1164','%f1165','%f1166','preloadKeys','%f1167','%f1168','adminPartition','resourceGroupManagement','%f1169','%f1170','%f1171','%f1172','createResourceGroup','%f1173','%f1174','%f1175','resourceGroupVcpuList','%f1176','%f1177','%f1178','%f1179','vcpuNumOrRange','%f1180','%f1181','%f1182','resourceGroupPriority','resourceGroupEnableDisable','%f1183','%f1184','%f1185','%f1186','alterResourceGroup','%f1187','setResourceGroup','%f1188','%f1189','%f1190','threadIdList','%f1191','%f1192','%f1193','%f1194','dropResourceGroup','utilityStatement','%f1195','%f1196','describeStatement','%f1197','%f1198','%f1199','%f1200','explainStatement','%f1201','%f1202','%f1203','%f1204','%f1205','%f1206','%f1207','%f1208','explainableStatement','%f1209','%f1210','%f1211','helpCommand','useCommand','restartServer','%f1212','expr','%f1213','%f1214','%f1215','%f1216','%f1217','%f1218','%f1219','%f1220','%f1221','%f1222','%f1223','boolPri','%f1224','%f1225','%f1226','compOp','%f1227','predicate','%f1228','%f1229','%f1230','%f1231','%f1232','%f1233','predicateOperations','%f1234','%f1235','%f1236','%f1237','bitExpr','%f1238','%f1239','%f1240','%f1241','%f1242','%f1243','simpleExpr','%f1244','%f1245','%f1246','%f1247','%f1248','%f1249','%f1250','%f1251','%f1252','%f1253','%f1254','%f1255','%f1256','%f1257','%f1258','%f1259','%f1260','%f1261','%f1262','%f1263','%f1264','%f1265','%f1266','%f1267','%f1268','%f1269','%f1270','arrayCast','%f1271','jsonOperator','%f1272','%f1273','%f1274','%f1275','%f1276','%f1277','%f1278','%f1279','%f1280','%f1281','%f1282','%f1283','%f1284','%f1285','%f1286','%f1287','%f1288','%f1289','%f1290','%f1291','%f1292','%f1293','sumExpr','%f1294','%f1295','%f1296','%f1297','%f1298','%f1299','%f1300','%f1301','%f1302','%f1303','%f1304','%f1305','%f1306','%f1307','%f1308','%f1309','%f1310','%f1311','%f1312','%f1313','%f1314','%f1315','%f1316','%f1317','%f1318','%f1319','%f1320','%f1321','%f1322','%f1323','%f1324','%f1325','%f1326','%f1327','%f1328','%f1329','%f1330','%f1331','%f1332','%f1333','%f1334','%f1335','groupingOperation','%f1336','%f1337','%f1338','%f1339','%f1340','windowFunctionCall','%f1341','%f1342','%f1343','%f1344','%f1345','%f1346','windowingClause','%f1347','%f1348','leadLagInfo','%f1349','%f1350','%f1351','nullTreatment','%f1352','%f1353','%f1354','jsonFunction','%f1355','inSumExpr','identListArg','%f1356','identList','%f1357','%f1358','%f1359','fulltextOptions','%f1360','%f1361','%f1362','%f1363','%f1364','%f1365','%f1366','%f1367','%f1368','%f1369','%f1370','%f1371','%f1372','%f1373','%f1374','runtimeFunctionCall','%f1375','%f1376','%f1377','%f1378','%f1379','%f1380','%f1381','%f1382','%f1383','%f1384','%f1385','%f1386','%f1387','%f1388','%f1389','%f1390','%f1391','%f1392','%f1393','%f1394','%f1395','%f1396','%f1397','%f1398','%f1399','%f1400','%f1401','%f1402','%f1403','%f1404','geometryFunction','%f1405','%f1406','timeFunctionParameters','fractionalPrecision','%f1407','weightStringLevels','%f1408','%f1409','%f1410','%f1411','%f1412','weightStringLevelListItem','%f1413','%f1414','%f1415','%f1416','dateTimeTtype','trimFunction','%f1417','%f1418','%f1419','%f1420','%f1421','%f1422','%f1423','substringFunction','%f1424','%f1425','%f1426','%f1427','%f1428','%f1429','%f1430','%f1431','%f1432','functionCall','%f1433','udfExprList','%f1434','%f1435','%f1436','udfExpr','variable','userVariable','%f1437','%f1438','systemVariable','internalVariableName','%f1439','%f1440','%f1441','%f1442','%f1443','whenExpression','thenExpression','elseExpression','%f1444','%f1445','%f1446','%f1447','%f1448','%f1449','%f1450','%f1451','%f1452','castType','%f1453','%f1454','%f1455','%f1456','%f1457','exprList','%f1458','%f1459','charset','notRule','not2Rule','interval','%f1460','intervalTimeStamp','exprListWithParentheses','exprWithParentheses','simpleExprWithParentheses','%f1461','orderList','%f1462','%f1463','%f1464','orderExpression','%f1465','groupList','%f1466','%f1467','groupingExpression','channel','%f1468','compoundStatement','returnStatement','ifStatement','%f1469','ifBody','%f1470','%f1471','thenStatement','%f1472','compoundStatementList','%f1473','%f1474','%f1475','%f1476','%f1477','caseStatement','%f1478','%f1479','elseStatement','%f1480','labeledBlock','unlabeledBlock','label','%f1481','%f1482','beginEndBlock','%f1483','labeledControl','unlabeledControl','loopBlock','whileDoBlock','repeatUntilBlock','%f1484','spDeclarations','%f1485','%f1486','spDeclaration','%f1487','%f1488','variableDeclaration','%f1489','%f1490','conditionDeclaration','spCondition','%f1491','sqlstate','%f1492','handlerDeclaration','%f1493','%f1494','%f1495','handlerCondition','cursorDeclaration','iterateStatement','leaveStatement','%f1496','getDiagnostics','%f1497','%f1498','%f1499','%f1500','%f1501','%f1502','%f1503','%f1504','%f1505','%f1506','signalAllowedExpr','statementInformationItem','%f1507','%f1508','conditionInformationItem','%f1509','%f1510','signalInformationItemName','%f1511','signalStatement','%f1512','%f1513','%f1514','%f1515','%f1516','%f1517','%f1518','%f1519','resignalStatement','%f1520','%f1521','%f1522','%f1523','%f1524','%f1525','%f1526','signalInformationItem','cursorOpen','cursorClose','%f1527','cursorFetch','%f1528','%f1529','%f1530','%f1531','%f1532','schedule','%f1533','%f1534','%f1535','%f1536','%f1537','columnDefinition','checkOrReferences','%f1538','checkConstraint','%f1539','constraintEnforcement','%f1540','%f1541','%f1542','%f1543','%f1544','%f1545','%f1546','%f1547','%f1548','tableConstraintDef','%f1549','%f1550','%f1551','%f1552','%f1553','%f1554','%f1555','%f1556','%f1557','%f1558','%f1559','%f1560','constraintName','fieldDefinition','%f1561','%f1562','%f1563','%f1564','%f1565','%f1566','%f1567','%f1568','%f1569','%f1570','%f1571','%f1572','%f1573','%f1574','%f1575','%f1576','%f1577','%f1578','%f1579','columnAttribute','%f1580','%f1581','%f1582','%f1583','%f1584','%f1585','%f1586','%f1587','%f1588','columnFormat','storageMedia','%f1589','%f1590','%f1591','gcolAttribute','%f1592','%f1593','%f1594','references','%f1595','%f1596','%f1597','%f1598','%f1599','%f1600','%f1601','%f1602','%f1603','%f1604','%f1605','deleteOption','%f1606','%f1607','keyList','%f1608','%f1609','%f1610','%f1611','keyPart','%f1612','keyListWithExpression','%f1613','%f1614','%f1615','keyPartOrExpression','keyListVariants','%f1616','%f1617','indexType','%f1618','indexOption','%f1619','commonIndexOption','%f1620','visibility','indexTypeClause','%f1621','fulltextIndexOption','spatialIndexOption','dataTypeDefinition','%f1622','%f1623','%f1624','%f1625','%f1626','%f1627','%f1628','%f1629','%f1630','%f1631','%f1632','%f1633','%f1634','%f1635','%f1636','%f1637','%f1638','%f1639','%f1640','%f1641','%f1642','%f1643','%f1644','%f1645','%f1646','%f1647','%f1648','%f1649','%f1650','dataType','%f1651','%f1652','%f1653','%f1654','%f1655','%f1656','%f1657','%f1658','%f1659','%f1660','%f1661','%f1662','nchar','%f1663','realType','fieldLength','%f1664','%f1665','fieldOptions','%f1666','%f1667','%f1668','%f1669','charsetWithOptBinary','%f1670','%f1671','%f1672','ascii','%f1673','unicode','wsNumCodepoints','typeDatetimePrecision','charsetName','%f1674','collationName','%f1675','%f1676','%f1677','createTableOptions','%f1678','%f1679','%f1680','%f1681','createTableOptionsSpaceSeparated','%f1682','%f1683','%f1684','%f1685','%f1686','%f1687','%f1688','%f1689','%f1690','%f1691','%f1692','%f1693','%f1694','%f1695','%f1696','%f1697','%f1698','%f1699','%f1700','createTableOption','%f1701','%f1702','%f1703','%f1704','%f1705','%f1706','%f1707','%f1708','%f1709','%f1710','%f1711','%f1712','%f1713','%f1714','%f1715','%f1716','%f1717','%f1718','%f1719','%f1720','%f1721','ternaryOption','%f1722','%f1723','defaultCollation','%f1724','%f1725','defaultEncryption','%f1726','%f1727','defaultCharset','%f1728','%f1729','%f1730','partitionClause','%f1731','%f1732','%f1733','%f1734','%f1735','%f1736','partitionTypeDef','%f1737','%f1738','%f1739','%f1740','%f1741','subPartitions','%f1742','%f1743','%f1744','%f1745','partitionKeyAlgorithm','%f1746','%f1747','partitionDefinitions','%f1748','%f1749','%f1750','%f1751','%f1752','partitionDefinition','%f1753','%f1754','%f1755','%f1756','%f1757','%f1758','%f1759','%f1760','%f1761','partitionValuesIn','%f1762','%f1763','%f1764','%f1765','%f1766','%f1767','%f1768','%f1769','%f1770','partitionOption','%f1771','%f1772','%f1773','subpartitionDefinition','%f1774','partitionValueItemListParen','%f1775','%f1776','partitionValueItem','definerClause','ifExists','ifNotExists','%f1777','procedureParameter','%f1778','%f1779','functionParameter','collate','%f1780','typeWithOptCollate','schemaIdentifierPair','%f1781','viewRefList','%f1782','%f1783','%f1784','updateList','%f1785','%f1786','updateElement','%f1787','charsetClause','%f1788','fieldsClause','%f1789','fieldTerm','%f1790','linesClause','lineTerm','%f1791','%f1792','userList','%f1793','%f1794','%f1795','createUserList','%f1796','%f1797','%f1798','alterUserList','%f1799','%f1800','%f1801','createUserEntry','%f1802','%f1803','%f1804','%f1805','%f1806','%f1807','%f1808','%f1809','%f1810','%f1811','%f1812','%f1813','%f1814','%f1815','%f1816','alterUserEntry','%f1817','%f1818','%f1819','%f1820','%f1821','%f1822','%f1823','%f1824','%f1825','%f1826','%f1827','%f1828','%f1829','%f1830','%f1831','%f1832','%f1833','retainCurrentPassword','discardOldPassword','replacePassword','%f1834','userIdentifierOrText','%f1835','%f1836','%f1837','%f1838','user','likeClause','likeOrWhere','onlineOption','noWriteToBinLog','usePartition','%f1839','%f1840','fieldIdentifier','columnName','%f1841','%f1842','columnInternalRef','%f1843','columnInternalRefList','%f1844','%f1845','columnRef','insertIdentifier','indexName','indexRef','%f1846','tableWild','%f1847','%f1848','schemaName','schemaRef','procedureName','procedureRef','functionName','functionRef','triggerName','triggerRef','viewName','viewRef','tablespaceName','tablespaceRef','logfileGroupName','logfileGroupRef','eventName','eventRef','udfName','serverName','serverRef','engineRef','tableName','filterTableRef','%f1849','tableRefWithWildcard','%f1850','%f1851','%f1852','%f1853','%f1854','tableRef','%f1855','tableRefList','%f1856','%f1857','%f1858','tableAliasRefList','%f1859','%f1860','parameterName','labelIdentifier','labelRef','roleIdentifier','roleRef','pluginRef','componentRef','resourceGroupRef','windowName','pureIdentifier','%f1861','%f1862','identifier','%f1863','identifierList','%f1864','%f1865','identifierListWithParentheses','%f1866','qualifiedIdentifier','%f1867','simpleIdentifier','%f1868','%f1869','%f1870','%f1871','dotIdentifier','ulong_number','real_ulong_number','ulonglong_number','real_ulonglong_number','%f1872','%f1873','literal','%f1874','signedLiteral','%f1875','stringList','%f1876','%f1877','textStringLiteral','%f1878','textString','textStringHash','%f1879','%f1880','textLiteral','%f1881','%f1882','textStringNoLinebreak','%f1883','textStringLiteralList','%f1884','%f1885','numLiteral','boolLiteral','nullLiteral','temporalLiteral','floatOptions','standardFloatOptions','precision','textOrIdentifier','lValueIdentifier','roleIdentifierOrText','sizeNumber','parentheses','equal','optionType','varIdentType','setVarIdentType','identifierKeyword','%f1886','%f1887','%f1888','%f1889','%f1890','identifierKeywordsAmbiguous1RolesAndLabels','identifierKeywordsAmbiguous2Labels','labelKeyword','%f1891','%f1892','%f1893','identifierKeywordsAmbiguous3Roles','identifierKeywordsUnambiguous','%f1894','%f1895','%f1896','roleKeyword','%f1897','%f1898','%f1899','lValueKeyword','identifierKeywordsAmbiguous4SystemVariables','roleOrIdentifierKeyword','%f1900','%f1901','%f1902','roleOrLabelKeyword','%f1903','%f1904','%f1905','%f1906','%f1907','%f1908','%f1909'],'grammar'=>[0=>[[-1],[2001,2003]],1=>[[2004],[2873]],2=>[[-1],[0]],3=>[[755,2002],[-1]],4=>[[2009],[2221],[2414],[2470],[2476],[2005],[2479],[2485],[2504],[2508],[2524],[2571],[2598],[2603],[2856],[2860],[2934],[3079],[2006],[3103],[3278],[3301],[3314],[3361],[2007],[3443],[3534],[2008],[3945],[3954]],5=>[[2477]],6=>[[3090]],7=>[[3498]],8=>[[3925]],9=>[[11,2013]],10=>[[2191]],12=>[[2285],[0]],13=>[[2071],[2031],[422,4388,2012],[206,4390,2012],[2212],[2042],[2175],[2010],[2060],[2067],[2014]],14=>[[2029]],15=>[[33]],16=>[[844],[2015]],19=>[[2018],[0]],18=>[[373,480,383,165]],20=>[[451,845,2019]],23=>[[2022],[0]],22=>[[373,480,383,165]],24=>[[451,845,200,57,4435,2023]],25=>[[156],[140]],26=>[[2025,844,846]],27=>[[451,847]],28=>[[482,2016,316,265],[2020],[2024],[2026],[2027]],29=>[[244,2028]],30=>[[4386],[0]],31=>[[109,2030,2034]],32=>[[615,112,139,357]],33=>[[2229,2033],[2229]],34=>[[2033],[2032]],391=>[[4273],[0]],43=>[[2044],[0]],46=>[[2047],[0]],48=>[[2049],[0]],53=>[[2054],[0]],55=>[[2056],[0]],57=>[[2058],[0]],42=>[[2391,170,4400,2043,2046,2048,2053,2055,2057]],44=>[[383,490,3972]],2023=>[[371],[0]],47=>[[383,79,4023,418]],49=>[[453,590,4435]],52=>[[2051],[0]],51=>[[383,514]],54=>[[156],[140,2052]],56=>[[75,4469]],58=>[[147,3869]],59=>[[2062],[0]],60=>[[288,217,4398,4,603,4469,2059]],64=>[[2065,2064],[2065],[0]],62=>[[2066,2064]],2157=>[[750],[0]],65=>[[4157,2066]],66=>[[2345],[2359],[2362]],67=>[[503,4403,2318]],427=>[[4363],[0]],73=>[[2074],[0]],70=>[[2077],[0]],71=>[[2427,2073,574,4414,2070]],72=>[[232]],74=>[[2072]],78=>[[2079],[0]],80=>[[2081],[0]],77=>[[2078,2090],[2083,2080],[4216],[2173]],79=>[[2087,750]],81=>[[4216],[2173]],84=>[[2085],[0]],83=>[[2087,2084],[2112]],85=>[[750,2112]],88=>[[2089,2088],[2089],[0]],87=>[[2117,2088]],89=>[[750,2117]],90=>[[141,572],[234,572],[2105],[2092]],91=>[[722],[723]],92=>[[2091]],1441=>[[4364],[0]],1273=>[[3296,3273],[3296],[0]],1277=>[[3298,3277],[3298],[0]],107=>[[2108],[0]],104=>[[2170],[0]],105=>[[4,405,3441,2106],[148,405,4437],[438,405,3441,2174],[388,405,3441,2174,3441],[14,405,3441,2174],[62,405,2174,3273],[455,405,3441,2174,3277],[67,405,3441,4451],[597,405,2174],[454,405,3441,2107],[172,405,4435,645,574,4414,2104],[2109],[2110]],106=>[[4237],[404,4451]],108=>[[4437,248,4237]],109=>[[141,405,2174,572]],110=>[[234,405,2174,572]],115=>[[2116,2115],[2116],[0]],112=>[[2113,2115]],113=>[[2126],[4161]],114=>[[2126],[2117],[4161]],116=>[[750,2114]],117=>[[2162],[2165],[2170]],136=>[[72],[0]],128=>[[2153],[0]],147=>[[2148],[0]],2282=>[[4281],[0]],126=>[[4,2136,2129],[4,3993],[55,2136,4372,4435,4007,2128],[348,2136,4372,4007,2128],[148,2138],[140,263],[156,263],[11,2136,4372,2142],[2143],[2144],[2145],[2146],[453,2147,4405],[2149],[94,590,3847,2151,4282],[198],[393,45,2157],[2152]],1977=>[[3979],[0]],129=>[[4435,4007,3977,2128],[753,2242,748]],130=>[[4372]],131=>[[4372],[0]],132=>[[2131]],133=>[[2130],[2132]],134=>[[62,4435]],135=>[[86,4435]],137=>[[2154],[0]],138=>[[2136,4372,2137],[199,265,2133],[420,265],[2840,4380],[2134],[2135]],139=>[[3854]],140=>[[2139],[4458]],141=>[[506,4082]],142=>[[506,128,2140],[148,128],[2141]],143=>[[11,236,4380,4082]],144=>[[11,62,4435,3983]],145=>[[11,86,4435,3983]],146=>[[453,72,4372,590,4435]],148=>[[590],[17]],149=>[[453,2840,4380,590,4379]],150=>[[128]],151=>[[2150],[4150]],152=>[[615,403]],153=>[[6,4435],[191]],154=>[[471],[49]],2071=>[[2724],[0]],159=>[[2160,2159],[2160],[0]],157=>[[4442,4071,2159]],160=>[[750,4442,4071]],2262=>[[763],[0]],162=>[[9,4262,2163]],163=>[[128],[4435]],165=>[[287,4262,2166]],166=>[[128],[4435]],167=>[[2165],[0]],168=>[[2162],[0]],169=>[[2162,2167],[2165,2168]],170=>[[2172]],171=>[[645],[646]],172=>[[2171,625]],173=>[[452,403]],174=>[[10],[4437]],175=>[[572,4396,2189]],176=>[[4],[148]],180=>[[2179,2180],[2179],[0]],179=>[[4157,2208]],184=>[[2182],[0]],182=>[[2208,2180]],183=>[[434],[436]],185=>[[55,111,4469,2184],[2183],[371,1]],186=>[[2185]],187=>[[2200]],188=>[[2200],[0]],189=>[[2176,111,4469,2188],[2186],[453,590,4435],[2187]],467=>[[2194],[0]],191=>[[605,572,4396,506,2192,2467]],192=>[[724],[725]],196=>[[2197,2196],[2197],[0]],194=>[[2198,2196]],197=>[[4157,2198]],198=>[[2359]],202=>[[2203,2202],[2203],[0]],200=>[[2205,2202]],203=>[[4157,2205]],205=>[[238,4262,4487],[2350],[2352],[2359],[2206],[2362],[2369]],206=>[[2361]],208=>[[238,4262,4487],[2350],[2352]],374=>[[2376],[0]],372=>[[2378],[0]],212=>[[2374,2391,2372,636,4394,2214]],1233=>[[4374],[0]],214=>[[3233,17,2216]],215=>[[2218],[0]],216=>[[2251,2215]],219=>[[2220],[0]],218=>[[645,2219,62,391]],220=>[[50],[284]],221=>[[97,2225]],222=>[[2408]],223=>[[2412]],224=>[[2328]],225=>[[2228],[2233],[2270],[2258],[2280],[2308],[2373],[2382],[2290],[2316],[2324],[2396],[2222],[2223],[2224]],1391=>[[4275],[0]],227=>[[2229,2227],[2229],[0]],228=>[[109,3391,4385,2227]],229=>[[4212],[4206],[2230]],230=>[[4209]],441=>[[577],[0]],233=>[[2441,574,3391,4405,2240]],236=>[[2235],[0]],235=>[[753,2242,748]],237=>[[4156],[0]],238=>[[4216],[0]],239=>[[2248],[0]],240=>[[275,4414],[753,275,4414,748],[2236,2237,2238,2239]],243=>[[2244,2243],[2244],[0]],242=>[[2245,2243]],244=>[[750,2245]],245=>[[3978],[3993]],249=>[[2250],[0]],762=>[[17],[0]],248=>[[2249,2762,2251]],250=>[[458],[232]],251=>[[2608],[2636]],252=>[[755],[0]],253=>[[97,2254,2252,-1]],254=>[[2258],[2270],[2280]],265=>[[2266],[0]],269=>[[2283,2269],[2283],[0]],258=>[[2391,422,2261,4387,753,2265,748,2269,3869]],260=>[[3391]],261=>[[2260]],264=>[[2263,2264],[2263],[0]],263=>[[750,4277]],266=>[[4277,2264]],277=>[[2278],[0]],270=>[[2391,206,2273,4389,753,2277,748,474,4283,2269,3869]],272=>[[3391]],273=>[[2272]],276=>[[2275,2276],[2275],[0]],275=>[[750,4280]],278=>[[4280,2276]],279=>[[8],[0]],280=>[[2279,206,4401,474,2281,520,4469]],281=>[[556],[249],[437],[126]],283=>[[2286],[4023,137]],284=>[[2283,2284],[2283]],285=>[[2284]],286=>[[75,4469],[267,537],[373,537],[90,537],[433,537,112],[347,537,112],[537,496,2287]],287=>[[130],[250]],428=>[[2169],[0]],290=>[[2427,2299,2428]],291=>[[4083],[0]],292=>[[4379,2291]],2000=>[[2302],[0]],294=>[[2292],[4000]],295=>[[609],[0]],2001=>[[4078,4001],[4078],[0]],1988=>[[4085,3988],[4085],[0]],1991=>[[4086,3991],[4086],[0]],299=>[[2295,236,2294,2306,4001],[205,236,4379,2306,3988],[523,236,4379,2306,3991]],2002=>[[4379],[0]],304=>[[2305],[0]],302=>[[4002,2304]],303=>[[621],[599]],305=>[[2303,4076]],306=>[[383,4414,4073]],307=>[[2311],[0]],308=>[[288,217,4397,4,2309,4469,2307]],309=>[[603],[440]],313=>[[2314,2313],[2314],[0]],311=>[[2315,2313]],314=>[[4157,2315]],315=>[[2345],[2347],[2356],[2359],[2362],[2365]],316=>[[503,4402,199,112,648,4484,2318]],319=>[[2320,2319],[2320],[0]],318=>[[390,753,2321,2319,748]],320=>[[750,2321]],321=>[[224,4469],[109,4469],[618,4469],[406,4469],[519,4469],[398,4469],[413,4450]],325=>[[2326],[0]],323=>[[2336],[0]],324=>[[572,4395,2329,2325,2323]],326=>[[620,288,217,4398]],328=>[[605,572,4395,4,2334,2467]],329=>[[2333],[4,2334]],332=>[[2331],[0]],331=>[[4,2334]],333=>[[2332]],334=>[[111,4469]],338=>[[2339,2338],[2339],[0]],336=>[[2340,2338]],339=>[[4157,2340]],340=>[[2345],[2350],[2352],[2354],[2356],[2359],[2341],[2362],[2365],[2342],[2343]],341=>[[2361]],342=>[[2367]],343=>[[2369]],345=>[[238,4262,4487]],347=>[[2348,4262,4487]],348=>[[604],[441]],350=>[[23,4262,4487]],352=>[[324,4262,4487]],354=>[[181,4262,4487]],356=>[[368,4262,4451]],2257=>[[553],[0]],359=>[[4257,163,4262,4404]],361=>[[848,4262,4463]],362=>[[2363]],363=>[[638],[374]],365=>[[75,4262,4469]],367=>[[189,4262,4487]],369=>[[158,4262,4463]],370=>[[2375],[0]],373=>[[2370,2391,2372,636,4393,2214]],375=>[[394,458,2374],[2376]],376=>[[9,763,2377]],377=>[[602],[335],[578]],378=>[[537,496,2379]],379=>[[130],[250]],381=>[[2388],[0]],382=>[[2391,594,2385,4391,2386,2387,383,4414,200,153,487,2381,3869]],384=>[[3391]],385=>[[2384]],386=>[[28],[6]],387=>[[242],[614],[133]],388=>[[2390]],389=>[[197],[415]],390=>[[2389,4484]],398=>[[2399],[0]],403=>[[2404],[0]],405=>[[2406],[0]],396=>[[2391,170,3391,4399,383,490,3972,2398,2403,2405,147,3869]],399=>[[383,79,4023,418]],402=>[[2401],[0]],401=>[[383,514]],404=>[[156],[140,2402]],406=>[[75,4469]],408=>[[659,3391,3264]],411=>[[2413,2411],[2413],[0]],412=>[[394,458,523,718,710,4453,2411],[523,718,710,3391,4453,2411]],413=>[[357,580,4472],[715,580,4472],[717,4472,230,45,4453],[716,580,4472]],414=>[[148,2418]],415=>[[2464]],416=>[[2466]],417=>[[2468]],418=>[[2420],[2422],[2424],[2426],[2429],[2431],[2440],[2444],[2449],[2457],[2460],[2415],[2416],[2417]],939=>[[4274],[0]],420=>[[109,2939,4386]],422=>[[170,2939,4400]],424=>[[206,2939,4390]],426=>[[422,2939,4388]],429=>[[2427,236,4380,383,4414,2428]],436=>[[2437],[0]],431=>[[288,217,4398,2436]],435=>[[2434,2435],[2434],[0]],434=>[[4157,2438]],437=>[[2438,2435]],438=>[[2362],[2359]],440=>[[503,2939,4403]],446=>[[2447],[0]],444=>[[2441,2445,2939,4416,2446]],445=>[[574],[571]],447=>[[471],[49]],454=>[[2455],[0]],449=>[[572,4396,2454]],453=>[[2452,2453],[2452],[0]],452=>[[4157,2438]],455=>[[2438,2453]],457=>[[594,2939,4392]],461=>[[2462],[0]],460=>[[636,2939,4286,2461]],462=>[[471],[49]],464=>[[659,2939,3264]],466=>[[523,718,710,2939,4453]],468=>[[605,572,4396,2467]],472=>[[2473,2472],[2473],[0]],470=>[[453,2471,2474,2472]],471=>[[574],[571]],473=>[[750,2474]],474=>[[4414,590,4405]],475=>[[574],[0]],476=>[[597,2475,4414]],477=>[[234,574,203,4474]],481=>[[2482],[0]],479=>[[48,4388,2481]],1807=>[[3844],[0]],482=>[[753,3807,748]],487=>[[2488],[0]],484=>[[2503,2484],[2503],[0]],485=>[[2487,133,2484,2500]],486=>[[2714]],488=>[[2486]],489=>[[2829]],493=>[[2491],[0]],491=>[[2489]],1415=>[[2765],[0]],494=>[[2501],[0]],1646=>[[2723],[0]],855=>[[2654],[0]],498=>[[4420,621,2728,3415],[4414,2493,2494,3415,3646,2855]],500=>[[203,2498],[4420,203,2728,3415]],501=>[[2502]],502=>[[405,753,4437,748]],503=>[[431],[295],[431],[232]],504=>[[147,2507]],505=>[[2756]],506=>[[3844]],507=>[[2505],[2506]],508=>[[219,2513]],1421=>[[2653],[0]],511=>[[66],[435,2514,3415,3421]],901=>[[2829],[0]],513=>[[4414,387,2901],[4435,2511]],514=>[[2515],[4435,2518]],515=>[[191],[367]],516=>[[191],[367],[419],[268]],517=>[[763],[769],[765],[768],[764]],518=>[[2516],[2517,753,2555,748]],519=>[[2534],[0]],852=>[[232],[0]],596=>[[248],[0]],791=>[[4365],[0]],523=>[[2562],[0]],524=>[[242,2519,2852,2596,4414,2791,2533,2523]],525=>[[2561]],531=>[[2527],[0]],527=>[[2525]],528=>[[2561]],532=>[[2530],[0]],530=>[[2528]],533=>[[2536,2531],[506,4290,2532],[2547]],534=>[[295],[131],[223]],538=>[[2539],[0]],536=>[[2538,2544]],546=>[[2541],[0]],539=>[[753,2546,748]],542=>[[2543,2542],[2543],[0]],541=>[[4378,2542]],543=>[[750,4378]],544=>[[2545,2550]],545=>[[626],[627]],547=>[[2251],[753,2546,748,2251]],736=>[[2555],[0]],552=>[[2553,2552],[2553],[0]],550=>[[753,2736,748,2552]],553=>[[750,753,2736,748]],558=>[[2559,2558],[2559],[0]],555=>[[2556,2558]],556=>[[3559],[128]],557=>[[3559],[128]],559=>[[750,2557]],561=>[[17,4435,3233]],562=>[[383,151,265,614,4290]],572=>[[2573],[0]],903=>[[284],[0]],574=>[[2575],[0]],667=>[[4295],[0]],568=>[[2577],[0]],668=>[[4297],[0]],669=>[[4301],[0]],571=>[[281,2576,2572,2903,237,4469,2574,248,574,4414,2791,2667,2568,2668,2669,2581]],573=>[[295],[82]],575=>[[458],[232]],576=>[[112],[653]],577=>[[484,230,45,4465]],583=>[[2584],[0]],579=>[[2588],[0]],585=>[[2586],[0]],581=>[[2583,2579,2585]],582=>[[278],[484]],584=>[[232,787,2582]],586=>[[506,4290]],587=>[[2590],[0]],588=>[[753,2587,748]],593=>[[2594,2593],[2594],[0]],590=>[[2591,2593]],591=>[[4377],[3816]],592=>[[4377],[3816]],594=>[[750,2592]],599=>[[2600],[0]],598=>[[458,2599,2596,4414,2791,2601]],600=>[[295],[131]],601=>[[2536],[506,4290],[2547]],635=>[[2742],[0]],603=>[[2608,2635],[2605]],605=>[[753,2605,748],[2608,2662,2635],[2608,2742,2662]],610=>[[2611],[0]],618=>[[2619],[0]],608=>[[2610,2616,2618]],609=>[[2714]],611=>[[2609]],616=>[[2621,3646,3421],[2636,3646,3421]],617=>[[2673]],619=>[[2617]],625=>[[2626,2625],[2626],[0]],621=>[[2628,2625]],622=>[[663]],623=>[[608],[2622]],631=>[[2827],[0]],626=>[[2623,2631,2628]],633=>[[2634,2633],[2634],[0]],628=>[[2629,2633]],629=>[[2637],[2636]],630=>[[2637],[2636]],632=>[[811,2631,2630]],634=>[[2632]],636=>[[753,2608,2635,748]],637=>[[2647],[2638],[2639]],638=>[[2732]],639=>[[2735]],640=>[[2738,2640],[2738],[0]],641=>[[2662],[0]],642=>[[2725],[0]],644=>[[2720],[0]],645=>[[2679],[0]],649=>[[2650],[0]],647=>[[497,2640,2756,2641,2642,3415,2644,2645,2649]],648=>[[2681]],650=>[[2648]],651=>[[2636]],652=>[[10],[143],[555],[223],[536],[531],[532],[534]],653=>[[276,2656]],654=>[[276,2660]],658=>[[2659],[0]],656=>[[2660,2658]],657=>[[750],[381]],659=>[[2657,2660]],660=>[[4435],[2661]],661=>[[754],[791],[788],[787]],662=>[[248,2671]],663=>[[4484],[3816]],664=>[[4484],[3816]],670=>[[2666,2670],[2666],[0]],666=>[[750,2664]],671=>[[396,4463,2667,2668,2669],[150,4463],[2663,2670]],677=>[[2678],[0]],673=>[[422,13,753,2677,748]],676=>[[2675],[0]],675=>[[750,787]],678=>[[787,2676]],679=>[[221,3559]],682=>[[2683,2682],[2683],[0]],681=>[[699,2684,2682]],683=>[[750,2684]],684=>[[4431,17,2685]],685=>[[753,2696,748]],695=>[[2704],[0]],697=>[[2698],[0]],699=>[[2700],[0]],692=>[[4431],[0]],701=>[[2702],[0]],696=>[[405,45,3857,3646,2695],[2697,2723,2695],[2699,3646,2704],[2692,2701,3646,2695]],698=>[[405,45,3857]],700=>[[405,45,3857]],702=>[[405,45,3857]],703=>[[2710],[0]],704=>[[2705,2706,2703]],705=>[[484],[432],[683]],706=>[[2707],[2708]],707=>[[698,693],[4452,693],[754,693],[247,3559,3850,693],[101,487]],708=>[[30,2709,15,2709]],709=>[[2707],[698,682],[4452,682],[754,682],[247,3559,3850,682]],710=>[[680,2711]],711=>[[101,487],[217],[697],[373,690]],712=>[[665],[0]],715=>[[2716,2715],[2716],[0]],714=>[[645,2712,2718,2715]],716=>[[750,2718]],718=>[[4435,3233,17,2651]],719=>[[2721],[0]],720=>[[217,45,3857,2719]],721=>[[645,481],[2722]],722=>[[645,99]],723=>[[393,45,3857]],724=>[[18],[134]],725=>[[203,2726]],726=>[[149],[2728]],729=>[[2730,2729],[2730],[0]],728=>[[2767,2729]],730=>[[750,2767]],733=>[[2734,2733],[2734],[0]],732=>[[626,2737,2733]],734=>[[750,2737]],735=>[[574,4414]],737=>[[487,753,2736,748]],738=>[[2652],[535],[2739],[2740]],739=>[[533]],740=>[[325,763,4451]],741=>[[2745,2741],[2745]],742=>[[2741]],747=>[[2748],[0]],750=>[[2751],[0]],745=>[[200,2752,2747,2750],[287,251,508,346]],746=>[[668,4420]],748=>[[2746]],749=>[[2754]],751=>[[2749]],752=>[[614],[2753]],753=>[[508]],754=>[[669,670],[671]],758=>[[2759,2758],[2759],[0]],756=>[[2757,2758]],757=>[[2761],[775]],759=>[[750,2761]],1813=>[[2763],[0]],761=>[[4382],[3559,3813]],763=>[[2762,2764]],764=>[[4435],[4463]],765=>[[643,3559]],771=>[[2774,2771],[2774],[0]],767=>[[2770,2771]],768=>[[4435]],769=>[[2768],[732]],770=>[[2789],[752,2769,2772,747]],772=>[[2789,2771]],775=>[[2776],[0]],774=>[[2783,2767,2775],[2787,2767,2777],[2780,2789]],776=>[[383,3559],[621,4440]],777=>[[383,3559],[621,4440]],778=>[[239],[0]],786=>[[395],[0]],780=>[[359,2778,261],[359,2781,2786,261]],781=>[[272],[478]],784=>[[2785],[0]],783=>[[2784,261],[555]],785=>[[239],[98]],787=>[[2788,2786,261]],788=>[[272],[478]],789=>[[2794],[2795],[2799],[2806],[2790]],790=>[[2809]],793=>[[2834],[0]],794=>[[4414,2791,2901,2793]],795=>[[753,2796,748]],796=>[[2794],[2795]],801=>[[2802],[0]],799=>[[2651,2901,2801],[2805]],800=>[[4374]],802=>[[2800]],805=>[[726,2651,2901,3233]],806=>[[753,2807,748]],807=>[[2728],[2806]],809=>[[701,753,3559,750,4463,2811,748,2901]],812=>[[2813,2812],[2813],[0]],811=>[[71,753,2817,2812,748]],813=>[[750,2817]],819=>[[2820],[0]],1606=>[[174],[0]],1749=>[[2823],[0]],817=>[[4435,200,703],[4435,4117,2819,3606,704,4463,3749],[702,704,4463,2811]],818=>[[4281]],820=>[[2818]],821=>[[2825],[0]],822=>[[2824],[0]],823=>[[2824,2821],[2825,2822]],824=>[[2826,383,700]],825=>[[2826,383,165]],826=>[[165],[376],[128,4463]],827=>[[143],[10]],831=>[[2832],[0]],829=>[[2831,4435]],830=>[[763]],832=>[[17],[2830]],833=>[[2838,2833],[2838]],834=>[[2833]],836=>[[2843],[0]],837=>[[2846],[0]],838=>[[2839,2840,2836,753,2846,748],[620,2840,2836,753,2837,748]],839=>[[198],[232]],840=>[[265],[236]],1995=>[[2840],[0]],842=>[[420,265],[609,3995]],843=>[[200,2844]],844=>[[261],[393,45],[217,45]],847=>[[2848,2847],[2848],[0]],846=>[[2849,2847]],848=>[[750,2849]],849=>[[4435],[420]],858=>[[2859],[0]],904=>[[295],[0]],856=>[[2858,614,2904,2852,2728,506,4290,3415,3646,2855]],857=>[[2714]],859=>[[2857]],860=>[[2865],[2882],[2894],[2906]],861=>[[2875],[0]],881=>[[647],[0]],867=>[[2868],[0]],870=>[[2871],[0]],865=>[[543,592,2861],[77,2881,2867,2870]],1101=>[[373],[0]],868=>[[15,3101,54]],871=>[[3101,450]],873=>[[29,2881]],876=>[[2877,2876],[2877],[0]],875=>[[2878,2876]],877=>[[750,2878]],878=>[[645,85,517],[2880]],879=>[[649],[386]],880=>[[435,2879]],882=>[[489,4435],[480,2881,2892],[450,489,4435]],890=>[[2885],[0]],885=>[[15,3101,54]],891=>[[2888],[0]],888=>[[3101,450]],889=>[[489],[0]],892=>[[590,2889,4435],[2890,2891]],896=>[[2897,2896],[2897],[0]],894=>[[287,2895,2902,2896],[2898],[611,2900]],895=>[[571],[574]],897=>[[750,2902]],898=>[[287,244,200,27]],899=>[[244]],900=>[[571],[574],[2899]],902=>[[4414,2901,2905]],905=>[[435,2903],[2904,649]],906=>[[651,2920]],907=>[[543],[29]],917=>[[2909],[0]],909=>[[261],[472]],912=>[[2911],[0]],911=>[[200,340]],918=>[[2914],[0]],914=>[[566,2912]],919=>[[2916],[0]],916=>[[384,407]],920=>[[2907,2927,2917],[159,2927,2918],[417,2927],[77,2927,2919],[480,2927],[439,2921]],921=>[[2925],[0]],924=>[[2923],[0]],923=>[[94,652]],925=>[[2924]],931=>[[2932],[0]],927=>[[4465,2931]],930=>[[2929],[0]],929=>[[750,4450]],932=>[[750,4465,2930]],937=>[[2938,2937],[2938],[0]],934=>[[428,2935,289,2936],[2959],[468,2949,2937],[2943],[3047],[2944],[2956],[2945]],935=>[[32],[316]],936=>[[590,4469],[28,3559]],938=>[[750,2949]],942=>[[2941],[0]],941=>[[2939,3820]],943=>[[468,658,2942]],944=>[[2979]],945=>[[3077]],946=>[[2951],[0]],1717=>[[10],[0]],1469=>[[3867],[0]],949=>[[316,2946],[2950],[514,3717,3469]],950=>[[430,47]],951=>[[2955]],952=>[[4451]],953=>[[4453]],954=>[[2952],[2953]],955=>[[590,2954]],956=>[[281,2957,203,316]],957=>[[112],[574,4414]],959=>[[55,316,590,2961,3469]],962=>[[2963,2962],[2963],[0]],961=>[[2964,2962]],963=>[[750,2964]],964=>[[300,763,4472],[729,763,4472],[297,763,4472],[318,763,4472],[303,763,4472],[304,763,4450],[298,763,4450],[305,763,4450],[299,763,4450],[314,763,4450],[308,763,4472],[307,763,4472],[317,763,4472],[309,763,4472],[738,763,2967],[310,763,4472],[313,763,4472],[315,763,4450],[311,763,4469],[312,763,4472],[712,763,4472],[713,763,4450],[319,763,4450],[233,763,2970],[735,763,4463],[736,763,4450],[296,763,4450],[737,763,2965],[739,763,4450],[742,763,2966],[2968]],965=>[[4355],[376]],966=>[[743],[383],[744]],967=>[[4472],[376]],968=>[[301,763,4472],[302,763,4452],[447,763,4472],[448,763,4450]],974=>[[2975],[0]],970=>[[753,2974,748]],973=>[[2972,2973],[2972],[0]],972=>[[750,4450]],975=>[[4450,2973]],980=>[[2981,2980],[2981],[0]],983=>[[2984],[0]],979=>[[55,459,522,590,2986,3469],[55,459,190,3024,2980,2983]],981=>[[750,3024]],982=>[[3867]],984=>[[2982]],987=>[[2988,2987],[2988],[0]],986=>[[2989,2987]],988=>[[750,2989]],989=>[[2990,763,4472],[2991,763,4472],[2992,763,4472],[2993,763,4472],[2994,763,4450],[2995,763,4472],[2996,763,4452],[2997,763,4450],[2998,763,4450],[2999,763,4450],[3000,763,4450],[817,763,4450],[3001,763,4450],[3002,763,4463],[3003,763,4450],[3004,763,4450],[3005,763,4472],[3006,763,4472],[3007,763,4472],[3008,763,4469],[3009,763,4472],[3010,763,4472],[3011,763,4472],[3012,763,4450],[3013,763,4472],[3014,763,2967],[3015,763,4472],[3016,763,4450],[729,763,4472],[233,763,2970],[841,763,4450],[737,763,2965],[739,763,4450],[742,763,2966],[842,763,4450],[447,763,4472],[448,763,4450]],990=>[[814],[297]],991=>[[820],[300]],992=>[[838],[318]],993=>[[823],[303]],994=>[[824],[304]],995=>[[821],[301]],996=>[[822],[302]],997=>[[813],[296]],998=>[[819],[319]],999=>[[816],[298]],1000=>[[826],[305]],1001=>[[818],[299]],1002=>[[815],[735]],1003=>[[839],[736]],1004=>[[827],[314]],1005=>[[828],[308]],1006=>[[829],[307]],1007=>[[830],[309]],1008=>[[832],[311]],1009=>[[833],[312]],1010=>[[834],[313]],1011=>[[831],[310]],1012=>[[835],[315]],1013=>[[837],[317]],1014=>[[836],[738]],1015=>[[825],[712]],1016=>[[840],[713]],1018=>[[3026],[0]],1020=>[[3030],[0]],1022=>[[3034],[0]],1023=>[[3039],[0]],1024=>[[460,763,753,3018,748],[461,763,753,3018,748],[462,763,753,3020,748],[463,763,753,3020,748],[464,763,753,3022,748],[465,763,753,3022,748],[466,763,753,3023,748]],1027=>[[3028,3027],[3028],[0]],1026=>[[4386,3027]],1028=>[[750,4386]],1031=>[[3032,3031],[3032],[0]],1030=>[[4406,3031]],1032=>[[750,4406]],1035=>[[3036,3035],[3036],[0]],1034=>[[3037,3035]],1036=>[[750,3037]],1037=>[[4472]],1040=>[[3041,3040],[3041],[0]],1039=>[[4284,3040]],1041=>[[750,4284]],1045=>[[3073],[0]],1048=>[[3049],[0]],1047=>[[543,514,3045,3048,3058,3469],[552,514,3045,3469]],1049=>[[613,3051]],1056=>[[3057,3056],[3057],[0]],1051=>[[3055,3056]],1052=>[[530],[528]],1053=>[[3052,763,4465]],1054=>[[529]],1055=>[[2968],[3053],[3054]],1057=>[[750,2968]],1058=>[[3071],[0]],1067=>[[3060],[0]],1060=>[[618,763,4465]],1068=>[[3062],[0]],1062=>[[406,763,4465]],1069=>[[3064],[0]],1064=>[[129,763,4465]],1070=>[[3066],[0]],1066=>[[409,763,4465]],1071=>[[3067,3068,3069,3070]],1074=>[[3075,3074],[3075],[0]],1073=>[[3076,3074]],1075=>[[750,3076]],1076=>[[449],[538]],1077=>[[3078,210]],1078=>[[543],[552]],1079=>[[417,4435,203,3080],[3083],[3081,417,4435]],1080=>[[4469],[3816]],1081=>[[123],[148]],1084=>[[3085],[0]],1083=>[[173,4435,3084]],1085=>[[621,3087]],1088=>[[3089,3088],[3089],[0]],1087=>[[3816,3088]],1089=>[[750,3816]],1090=>[[677,3097]],1096=>[[3092],[0]],1092=>[[200,459]],1093=>[[3100],[0]],1094=>[[244,203,4360,749,4450,230,45,4463,3093]],2183=>[[4489],[0]],1097=>[[284,112,139,4183,4463],[676,3096],[3094]],1099=>[[3102],[0]],1100=>[[3102],[112,139,4183,4463,3099]],1102=>[[467,3101,539]],1103=>[[3104],[3119],[3158],[3162],[3193],[3198],[3105]],1104=>[[3107]],1105=>[[3258]],1109=>[[3110],[0]],1107=>[[11,618,3109,3111]],1108=>[[4274]],1110=>[[3108]],1111=>[[3114],[3117,3122]],1112=>[[3118],[4360]],1113=>[[10],[369],[3264]],1114=>[[3112,128,659,3113]],1115=>[[4313]],1116=>[[4309]],1117=>[[3115],[3116]],1118=>[[618,4488]],1119=>[[97,618,3121,4309,3132,3122]],1120=>[[4275]],1121=>[[3120],[0]],1122=>[[3131],[0]],1123=>[[75],[812]],1124=>[[3123,4465]],1130=>[[3126],[0]],1126=>[[3124]],1127=>[[3137],[0]],1128=>[[3141],[0]],1129=>[[3144,3129],[3144],[0]],1131=>[[3127,3128,3129,3130]],1132=>[[3136],[0]],1135=>[[3134],[0]],1134=>[[128,659,3264]],1136=>[[3135]],1137=>[[467,3139]],1138=>[[539],[650],[369]],1139=>[[3251],[3138]],1142=>[[3143,3142],[3143]],1141=>[[645,3142]],1143=>[[322,4450],[327,4450],[321,4450],[328,4450]],1144=>[[2,3145],[406,3155],[740,3156],[741,4451]],1145=>[[287],[611]],1154=>[[3147],[0]],1147=>[[247,4451,122],[365],[128]],1148=>[[4451],[128]],1149=>[[4451,122],[128]],1152=>[[3151],[0]],1151=>[[128],[719]],1153=>[[467,101,3152]],1155=>[[177,3154],[705,3148],[706,247,3149],[3153]],1156=>[[4451],[698]],1160=>[[3161],[0]],1158=>[[148,618,3160,4305]],1159=>[[4274]],1161=>[[3159]],1162=>[[215,3176]],1165=>[[3164],[0]],1164=>[[645,660,391]],1166=>[[3225,590,4305,3165]],1210=>[[421],[0]],1168=>[[3225],[10,3210]],1175=>[[3170],[0]],1170=>[[645,215,391]],1218=>[[3223],[0]],1172=>[[3190],[0]],1173=>[[3180],[0]],1174=>[[3189],[0]],1176=>[[3166],[3168,383,3218,3245,590,3177,3172,3173,3174],[427,383,4360,590,3177,3175]],1177=>[[3178],[3179]],1178=>[[4309]],1179=>[[4305]],1180=>[[3182],[3183]],1181=>[[3256,3181],[3256]],1182=>[[645,3181]],1183=>[[645,215,391]],1184=>[[663,3264]],1185=>[[645,659,3187]],1186=>[[3184],[0]],1187=>[[3264],[10,3186],[369],[128]],1188=>[[3185],[0]],1189=>[[17,618,3188]],1190=>[[3191]],1191=>[[3137]],1194=>[[3195,3194],[3195],[0]],1193=>[[453,618,4360,590,4360,3194]],1195=>[[750,4360,590,4360]],1200=>[[3201],[0]],1213=>[[3214],[0]],1198=>[[477,3200,3211,3213]],1199=>[[4274]],1201=>[[3199]],1202=>[[3225,203,4305]],1203=>[[3215],[0]],1207=>[[3205],[0]],1205=>[[3203,203,4305]],1208=>[[383,3218,3245,3207]],1209=>[[3208],[750,215,391,203,4305]],1211=>[[3202],[3225,3215,203,4305],[10,3210,3209],[427,383,4360,203,4305]],1212=>[[232,610,618]],1214=>[[3212]],1215=>[[3217],[3222]],1217=>[[383,3218,3245]],1221=>[[3220],[0]],1220=>[[383,3218,3245]],1222=>[[3221]],1223=>[[574],[206],[422]],1226=>[[3227,3226],[3227],[0]],1225=>[[3231,3226]],1227=>[[750,3231]],1241=>[[3242],[0]],1230=>[[483],[0]],1231=>[[3235],[3236,3233],[3238],[3239],[215,391],[509,110],[97,3241],[287,571],[459,3243],[509,636],[11,3230]],1232=>[[792],[746,4484]],1234=>[[4486,3232],[4486,3233]],1235=>[[3234]],1236=>[[497],[242],[614],[443]],1237=>[[97],[148]],1238=>[[3237,659]],1239=>[[133],[616],[236],[148],[173],[451],[510],[423],[188],[427],[565],[170],[594]],1240=>[[483],[572],[618],[636]],1242=>[[577,571],[3240]],1243=>[[65],[514]],1246=>[[3247],[0]],1245=>[[775,3246],[4386,751,3249],[4386],[4414]],1247=>[[751,775]],1248=>[[4414]],1249=>[[775],[3248]],1253=>[[3254,3253],[3254],[0]],1251=>[[3255,3253]],1252=>[[15],[0]],1254=>[[3252,3255]],1255=>[[63,4465],[259,4465],[559,4465]],1256=>[[215,391],[322,4450],[327,4450],[321,4450],[328,4450]],1261=>[[3262],[0]],1258=>[[506,659,3264],[506,659,3259],[506,128,659,3260,590,3264],[506,659,10,3261]],1259=>[[369],[128]],1260=>[[3264],[369],[10]],1262=>[[663,3264]],1265=>[[3266,3265],[3266],[0]],1264=>[[3268,3265]],1266=>[[750,3268]],1269=>[[3270],[0]],1268=>[[4486,3269]],1270=>[[746,4484],[792]],1281=>[[3282],[0]],1285=>[[3286],[0]],1278=>[[14,3441,3279,4416,3281],[62,3283,4416,3273],[61,3284,4416,3285],[388,3441,3287,4416],[455,3441,3288,4416,3277]],1279=>[[574],[571]],1280=>[[3291]],1282=>[[3280]],1283=>[[574],[571]],1284=>[[574],[571]],1286=>[[431],[180]],1287=>[[574],[571]],1288=>[[574],[571]],1292=>[[3293],[0]],1294=>[[3295],[0]],1291=>[[614,674,383,4437,3292,3294],[148,674,383,4437]],1293=>[[645,787,675]],1295=>[[621,112,4463]],1296=>[[200,615],[3297]],1297=>[[431],[184],[333],[180],[56]],1298=>[[431],[180],[619]],1302=>[[3303],[0]],1304=>[[3305,3304],[3305],[0]],1301=>[[245,410,4435,520,4463],[245,664,4474,3302],[607,410,4428],[607,664,4429,3304]],1303=>[[506,3311]],1305=>[[750,4429]],1306=>[[214],[658]],1307=>[[3306],[0]],1308=>[[3307,3820,4489,3309]],1309=>[[383],[3559]],1312=>[[3313,3312],[3313],[0]],1311=>[[3308,3312]],1313=>[[750,3308]],1314=>[[506,3316]],1317=>[[3318],[0]],1316=>[[592,3334],[406,3317,4489,3325],[3331],[3348,3345],[4490,3355]],1318=>[[200,4360]],1319=>[[382,753,4465,748]],1320=>[[406,753,4465,748]],2343=>[[4353],[0]],2344=>[[4351],[0]],1325=>[[4465,4343,4344],[4465,4343,4344],[3319],[3320]],1328=>[[3327],[0]],1327=>[[200,4360]],1331=>[[406,3328,590,734,4343,4344]],1335=>[[3336],[0]],1337=>[[3338],[0]],1334=>[[3339,3335],[3341,3337]],1336=>[[750,3341]],1338=>[[750,3339]],1339=>[[435,3340]],1340=>[[649],[386]],1341=>[[258,274,3343]],1342=>[[76],[601]],1343=>[[456,435],[435,3342],[500]],1346=>[[3347,3346],[3347],[0]],1345=>[[3346]],1347=>[[750,3352]],1348=>[[3820,4489,3357],[4295],[3816,4489,3559],[3354,4489,3357],[356,3351]],1349=>[[128]],1351=>[[4489,3559],[4150,4282],[3349]],1352=>[[4490,3820,4489,3357],[3348]],1353=>[[4492],[0]],1354=>[[745,3353,3820]],1355=>[[3356,3345],[592,3334]],1356=>[[3820,4489,3357]],1357=>[[3559],[3358],[3360]],1358=>[[128],[383],[10],[32]],1359=>[[487],[710]],1360=>[[3359]],1361=>[[509,3430]],1362=>[[22]],1363=>[[4404],[10]],1364=>[[547],[354],[289]],1365=>[[203],[251]],1366=>[[32],[316]],1368=>[[225],[547,3434,3469]],1369=>[[33],[446]],1409=>[[3371],[0]],1371=>[[251,4465]],1410=>[[3373],[0]],1373=>[[203,4452]],1374=>[[180]],1413=>[[3376],[0]],1376=>[[3374]],1377=>[[236],[235],[263]],1378=>[[639],[166]],1381=>[[3380,3381],[3380],[0]],1380=>[[750,3439]],1419=>[[3383],[0]],1383=>[[3439,3381]],1420=>[[3385],[0]],1385=>[[200,430,787]],1386=>[[547],[631]],1387=>[[93]],1427=>[[3389],[0]],1389=>[[200,4360]],1390=>[[618,4360]],1392=>[[109,3391,4386],[170,4400],[206,4390],[422,4388],[574,4414],[594,4392],[636,4394],[3390]],1429=>[[4362],[0]],1406=>[[3431],[0]],1414=>[[3438],[0]],1432=>[[204],[0]],1422=>[[4490],[0]],1430=>[[3362],[110,3429],[3406,571,3414,3429],[3432,593,3414,3429],[169,3414,3429],[574,547,3414,3429],[387,571,3414,3429],[408],[163,3363,3364],[3406,71,3365,4414,3414,3429],[3366,289],[514,3368],[3369,169,3409,3410,3421,3469],[3413,3377,3437,4414,3414,3415],[4257,162],[95,753,775,748,3378],[639,3421],[166,3421],[426],[425,3419,3420,3421],[3422,3386,3429],[3432,424],[3847,3429],[70,3429],[3387],[421],[216,200,4360,621,4305],[216,3427],[316,547],[97,3392],[422,547,3429],[206,547,3429],[422,68,4388],[206,68,4390]],1431=>[[204],[3433]],1433=>[[180,3432]],1434=>[[3436],[0]],1435=>[[370],[0]],1436=>[[3435]],1437=>[[203],[251]],1438=>[[3437,4435]],1439=>[[40,255],[91,568],[400,185],[3440]],1440=>[[10],[96],[256],[334],[522],[567]],1449=>[[3450],[0]],1443=>[[33,4469],[47,236,3452,251,3444],[196,3441,3448],[266,3449,3559],[281,236,248,47,3485],[3451]],1444=>[[4435],[128]],1447=>[[3446,3447],[3446],[0]],1446=>[[750,3470]],1448=>[[3476],[3470,3447]],1450=>[[84],[430]],1451=>[[510]],1452=>[[3454],[3460]],1455=>[[3456,3455],[3456],[0]],1454=>[[3458,3455]],1456=>[[750,3458]],1492=>[[3462],[0]],1458=>[[4414,3492]],1460=>[[4414,405,753,2174,748,3492]],1461=>[[3465],[0]],1462=>[[2840,753,3461,748]],1463=>[[4435],[420]],1466=>[[3467,3466],[3467],[0]],1465=>[[3463,3466]],1467=>[[750,3463]],1468=>[[3474],[0]],1470=>[[3471],[3468,289],[445,289,3469],[3472],[3473]],1471=>[[136],[225],[421],[547],[617]],1472=>[[430,47]],1473=>[[389]],1474=>[[32],[163],[165],[208],[515]],1479=>[[3480],[0]],1476=>[[3477,3479]],1477=>[[571],[574]],1478=>[[3481],[0]],1480=>[[645,435,287],[4416,3478]],1481=>[[3482],[645,435,287]],1482=>[[200,179]],1486=>[[3487],[0]],1485=>[[4414,3497,3492,3486],[3489]],1487=>[[232,270]],1490=>[[3491,3490],[3491],[0]],1489=>[[3494,3490]],1491=>[[750,3494]],1495=>[[3496],[0]],1494=>[[4414,3492,3495]],1496=>[[232,270]],1497=>[[405,753,2174,748]],1498=>[[3503],[3522],[3524],[3533]],1518=>[[3507],[0]],1519=>[[3516],[0]],1520=>[[3517],[0]],1503=>[[97,709,217,4435,599,4183,3504,3518,3519,3520]],1504=>[[618],[710]],1509=>[[3510,3509],[3510],[0]],1507=>[[711,4183,3512,3509]],1510=>[[4157,3512]],1513=>[[3514],[0]],1512=>[[787,3513]],1514=>[[773,787]],1516=>[[708,4183,787]],1517=>[[156],[140]],1532=>[[198],[0]],1522=>[[11,709,217,4430,3518,3519,3520,3532]],1525=>[[3526],[0]],1524=>[[506,709,217,4435,3525]],1526=>[[200,3528]],1530=>[[3531,3530],[3531],[0]],1528=>[[4451,3530]],1531=>[[4157,4451]],1533=>[[148,709,217,4430,3532]],1534=>[[3542],[3537],[3555],[3556],[3535]],1535=>[[3557]],1539=>[[3540],[0]],1537=>[[3538,4414,3539]],1538=>[[178],[135],[134]],1540=>[[4465],[4377]],1549=>[[3550],[0]],1542=>[[3543,3549,3551]],1543=>[[178],[135],[134]],1544=>[[180]],1545=>[[404]],1546=>[[201,763,4484]],1547=>[[14,201,763,4484]],1548=>[[14]],1550=>[[3544],[3545],[3546],[3547],[3548]],1551=>[[2603],[3553],[3554]],1552=>[[2485],[2524],[2598],[2856]],1553=>[[3552]],1554=>[[200,84,4451]],1555=>[[222,4484]],1556=>[[620,4435]],1557=>[[714]],1558=>[[3567,3558],[3567],[0]],1559=>[[3561,3558]],1564=>[[3565],[0]],1561=>[[3571,3564],[3566]],1562=>[[596],[183],[610]],2040=>[[3848],[0]],1565=>[[257,4040,3562]],1566=>[[371,3559]],1567=>[[3568,3559],[654,3559],[3569,3559]],1568=>[[15],[770]],1569=>[[394],[772]],1570=>[[3573,3570],[3573],[0]],1571=>[[3577,3570]],1573=>[[257,4040,376],[3575,3574,2651],[3575,3577]],1574=>[[10],[16]],1575=>[[763],[777],[764],[765],[768],[769],[776]],1581=>[[3582],[0]],1577=>[[3589,3581]],1578=>[[668],[0]],1579=>[[733,3578,3855]],1582=>[[4040,3584],[3579],[521,275,3589]],1586=>[[3587],[0]],1584=>[[251,3585],[30,3589,15,3577],[275,3596,3586],[444,3589]],1585=>[[2651],[753,3844,748]],1587=>[[168,3596]],1588=>[[3590,3588],[3590],[0]],1589=>[[3596,3588]],1590=>[[760,3589],[3591,3589],[3592,3589],[3593,247,3559,3850],[3594,3589],[757,3589],[759,3589]],1591=>[[775],[762],[774],[145],[349]],1592=>[[778],[773]],1593=>[[778],[773]],1594=>[[779],[780]],1597=>[[3598,3597],[3598],[0]],1596=>[[3600,3597]],1598=>[[761,3600]],1601=>[[3602],[0]],1600=>[[3612,3601]],1602=>[[69,4484]],1613=>[[3614],[0]],1604=>[[3626],[0]],1605=>[[487],[0]],1607=>[[3725],[0]],1608=>[[3624],[0]],1881=>[[3559],[0]],1622=>[[3623,3622],[3623]],1611=>[[3828],[0]],1612=>[[4456],[3649],[3815,3613],[3808],[3741],[4377,3604],[754],[3615],[3616],[3617,3596],[3849,3596],[3605,753,3844,748],[3606,2651],[752,4435,3559,747],[320,3719,7,753,3589,3607,748],[32,3596],[3621],[52,753,3559,17,3838,3608,748],[51,3881,3622,3611,159],[94,753,3559,750,3838,748],[94,753,3559,621,4150,748],[128,753,4444,748],[626,753,4444,748],[247,3559,3850,778,3559]],1614=>[[4489,3559]],1615=>[[3692]],1616=>[[3698]],1617=>[[778],[773],[758]],1618=>[[247],[0]],2106=>[[4149],[0]],1620=>[[52,753,3559,21,586,843,3618,4463,17,113,4106,748]],1621=>[[3620]],1623=>[[3826,3827]],1624=>[[3625]],1625=>[[731]],1626=>[[3627],[3628]],1627=>[[766,4463]],1628=>[[767,4463]],1645=>[[143],[0]],1651=>[[3652],[0]],1655=>[[3656],[0]],1659=>[[3660],[0]],1664=>[[3665],[0]],1667=>[[3668],[0]],1670=>[[3671],[0]],1673=>[[3674],[0]],1676=>[[3677],[0]],1679=>[[3680],[0]],1682=>[[3683],[0]],1685=>[[3686],[0]],1687=>[[3688],[0]],1690=>[[3691],[0]],1649=>[[26,753,3645,3718,748,3651],[3653,753,3718,748,3655],[3657],[95,753,3717,775,748,3659],[95,753,3662,748,3664],[345,753,3645,3718,748,3667],[326,753,3645,3718,748,3670],[551,753,3718,748,3673],[632,753,3718,748,3676],[548,753,3718,748,3679],[635,753,3718,748,3682],[564,753,3645,3718,748,3685],[218,753,3645,3844,3646,3687,748,3690]],1650=>[[3705]],1652=>[[3650]],1653=>[[35],[36],[38]],1654=>[[3705]],1656=>[[3654]],1657=>[[3716]],1658=>[[3705]],1660=>[[3658]],1662=>[[3717,775],[3718],[143,3844]],1663=>[[3705]],1665=>[[3663]],1666=>[[3705]],1668=>[[3666]],1669=>[[3705]],1671=>[[3669]],1672=>[[3705]],1674=>[[3672]],1675=>[[3705]],1677=>[[3675]],1678=>[[3705]],1680=>[[3678]],1681=>[[3705]],1683=>[[3681]],1684=>[[3705]],1686=>[[3684]],1688=>[[499,4465]],1689=>[[3705]],1691=>[[3689]],1692=>[[672,753,3844,748]],1693=>[[3708],[0]],1697=>[[3712],[0]],1703=>[[3704],[0]],1698=>[[3699,4488,3705],[688,3855,3705],[3700,753,3559,3693,748,3697,3705],[3701,3854,3697,3705],[687,753,3559,750,3596,748,3703,3697,3705]],1699=>[[696],[694],[679],[678],[692]],1700=>[[686],[684]],1701=>[[681],[685]],1702=>[[191],[268]],1704=>[[203,3702]],1705=>[[691,3706]],1706=>[[4431],[2685]],1710=>[[3711],[0]],1708=>[[750,3709,3710]],1709=>[[4452],[754],[4435],[3816]],1711=>[[750,3559]],1712=>[[3713,689]],1713=>[[695],[232]],1715=>[[3705],[0]],1716=>[[667,753,3718,748,3715],[666,753,3718,750,3718,748,3715]],1718=>[[3717,3559]],1719=>[[3721],[753,3721,748]],1722=>[[3723,3722],[3723],[0]],1721=>[[4444,3722]],1723=>[[750,4444]],1726=>[[3727],[0]],1725=>[[251,41,346],[251,359,267,346,3726],[645,430,176]],1727=>[[645,430,176]],1742=>[[3743],[0]],2359=>[[4488],[0]],1744=>[[3745,3744],[3745]],1751=>[[3752],[0]],2030=>[[3775],[0]],1757=>[[3758],[0]],1761=>[[3762],[0]],1741=>[[60,753,3844,3742,748],[105,4359],[116,3854],[122,3854],[229,3854],[242,753,3559,750,3559,750,3559,750,3559,748],[247,753,3559,3744,748],[3750],[272,753,3559,750,3559,748],[343,3854],[350,3854],[478,753,3559,750,3559,748],[495,3854],[586,3854],[583,753,3559,3751,748],[3790],[618,4488],[626,3854],[656,3854],[3753,753,3559,750,3754,748],[100,4359],[108,4030],[3755,753,3559,750,247,3559,3850,748],[182,753,3850,203,3559,748],[213,753,3789,750,3559,748],[372,4030],[414,753,3589,251,3559,748],[3798],[569,4030],[3756,753,3852,750,3559,750,3559,748],[622,4359],[624,4030],[623,4030],[19,3854],[58,3854],[67,3853],[70,3854],[109,4488],[231,753,3559,750,3559,750,3559,748],[201,753,3559,750,3559,3757,748],[337,3854],[349,753,3559,750,3559,748],[3759],[3760],[429,3854],[457,753,3559,750,3559,748],[458,753,3559,750,3559,750,3559,748],[476,3854],[485,4488],[597,753,3559,750,3559,748],[640,753,3559,3761,748],[641,753,3559,17,60,748],[641,753,3559,3770,748],[3772]],1743=>[[621,4150]],1745=>[[750,3559]],1748=>[[3747],[0]],1747=>[[851,3838]],1750=>[[850,753,3596,750,4469,3748,3749,748]],1752=>[[750,3559]],1753=>[[5],[558]],1754=>[[3559],[247,3559,3850]],1755=>[[114],[115]],1756=>[[584],[585]],1758=>[[750,3559]],1759=>[[382,753,4469,748]],1760=>[[406,3854]],1762=>[[750,3559]],1768=>[[3764],[0]],1764=>[[17,60,4148]],1765=>[[3778]],1769=>[[3767],[0]],1767=>[[3765]],1770=>[[17,32,4148],[3768,3769],[750,4450,750,4450,750,4450]],1772=>[[3773],[211,753,3807,748],[279,3853],[351,3853],[352,3853],[353,3853],[411,753,3559,750,3559,748],[412,3853]],1773=>[[90,753,3559,750,3559,748]],1774=>[[3776],[0]],1775=>[[753,3774,748]],1776=>[[3777]],1777=>[[787]],1778=>[[274,3782]],1781=>[[3780,3781],[3780],[0]],1780=>[[750,3784]],1782=>[[4451,773,4451],[3784,3781]],1787=>[[3788],[0]],1784=>[[4451,3787]],1785=>[[18],[134]],1786=>[[476],[0]],1788=>[[3785,3786],[476]],1789=>[[116],[586],[113],[583]],1790=>[[595,753,3797,748]],1793=>[[3792],[0]],1792=>[[203,3559]],1797=>[[3559,3793],[269,3881,203,3559],[591,3881,203,3559],[43,3881,203,3559]],1798=>[[563,753,3559,3805,748]],1803=>[[3800],[0]],1800=>[[750,3559]],1804=>[[3802],[0]],1802=>[[200,3559]],1805=>[[750,3559,3803],[203,3559,3804]],1806=>[[3810],[0]],1808=>[[4432,753,3806,748],[4442,753,3807,748]],1811=>[[3812,3811],[3812],[0]],1810=>[[3814,3811]],1812=>[[750,3814]],1814=>[[3559,3813]],1815=>[[3816],[3819]],1816=>[[746,4484],[792]],1817=>[[4491],[0]],2445=>[[4449],[0]],1819=>[[745,3817,4484,4445]],1820=>[[3825],[128,4449]],1822=>[[4435,4445]],1824=>[[4485,4445]],1825=>[[3822],[3824]],1826=>[[642,3559]],1827=>[[582,3559]],1828=>[[154,3559]],2111=>[[4133],[0]],2116=>[[4141],[0]],1834=>[[249],[0]],2092=>[[4481],[0]],1838=>[[32,4111],[60,4111,4116],[4130,4111],[512,3834],[612,3834],[116],[586,4106],[113,4106],[126,4092],[656],[3839],[3840],[3842]],1839=>[[262]],1840=>[[4132]],1841=>[[4482],[0]],1842=>[[195,3841]],1845=>[[3846,3845],[3846],[0]],1844=>[[3559,3845]],1846=>[[750,3559]],1847=>[[60,506],[58]],1848=>[[371],[800]],1849=>[[771],[800]],1850=>[[3852],[3851]],1851=>[[494],[341],[342],[226],[228],[227],[119],[121],[120],[118],[655]],1852=>[[337],[495],[343],[229],[122],[640],[350],[429],[656]],1853=>[[753,3844,748]],1854=>[[753,3559,748]],1855=>[[753,3596,748]],1858=>[[3859,3858],[3859],[0]],1857=>[[3861,3858]],1859=>[[750,3861]],1861=>[[3559,4071]],1864=>[[3865,3864],[3865],[0]],1863=>[[3866,3864]],1865=>[[750,3866]],1866=>[[3559]],1867=>[[3868]],1868=>[[200,57,4472]],1869=>[[2004],[3870],[3871],[3884],[3889],[3890],[3896],[3897],[3923],[3922],[3963],[3966],[3964]],1870=>[[475,3559]],1871=>[[231,3873,159,231]],1874=>[[3875],[0]],1873=>[[3559,3876,3874]],1875=>[[155,3873],[154,3878]],1876=>[[582,3878]],1879=>[[3880,3879],[3880]],1878=>[[3879]],1880=>[[3869,755]],1885=>[[3886,3885],[3886]],1883=>[[3887],[0]],1884=>[[51,3881,3885,3883,159,51]],1886=>[[3826,3876]],1887=>[[154,3878]],1895=>[[4425],[0]],1889=>[[3891,3894,3895]],1890=>[[3894]],1891=>[[4424,749]],1892=>[[3902],[0]],1893=>[[3878],[0]],1894=>[[29,3892,3893,159]],1896=>[[3891,3897,3895]],1897=>[[3898],[3899],[3900]],1898=>[[294,3878,159,294]],1899=>[[644,3559,147,3878,159,644]],1900=>[[457,3878,613,3559,159,457]],1903=>[[3904,3903],[3904]],1902=>[[3903]],1904=>[[3905,755]],1905=>[[3908],[3911],[3916],[3921]],1909=>[[3910],[0]],1908=>[[127,4437,4117,4282,3909]],1910=>[[128,3559]],1911=>[[127,4435,83,200,3912]],1912=>[[4450],[3914]],1913=>[[627],[0]],1914=>[[526,3913,4469]],1918=>[[3919,3918],[3919],[0]],1916=>[[127,3917,219,200,3920,3918,3869]],1917=>[[92],[175],[605]],1919=>[[750,3920]],1920=>[[3912],[4435],[527],[3848,202],[525]],1921=>[[127,4435,106,200,2603]],1922=>[[260,4425]],1923=>[[271,4425]],1927=>[[3928],[0]],1925=>[[207,3927,138,3935]],1926=>[[540]],1928=>[[101],[3926]],1933=>[[3930,3933],[3930],[0]],1930=>[[750,3937]],1934=>[[3932,3934],[3932],[0]],1932=>[[750,3940]],1935=>[[3937,3933],[83,3936,3940,3934]],1936=>[[4456],[3815],[4442]],1937=>[[3938,763,3939]],1938=>[[3815],[4435]],1939=>[[377],[485]],1940=>[[3941,763,3942]],1941=>[[3815],[4435]],1942=>[[3943],[473]],1943=>[[64],[557],[87],[89],[88],[53],[492],[576],[73],[107],[336],[355]],1950=>[[3951],[0]],1945=>[[511,3946,3950]],1946=>[[4435],[3914]],1949=>[[3948,3949],[3948],[0]],1948=>[[750,3962]],1951=>[[506,3962,3949]],1955=>[[3956],[0]],1960=>[[3961],[0]],1954=>[[469,3955,3960]],1956=>[[4435],[3914]],1959=>[[3958,3959],[3958],[0]],1958=>[[750,3962]],1961=>[[506,3962,3959]],1962=>[[3943,763,3936]],1963=>[[387,4435]],1964=>[[66,4435]],1968=>[[3969],[0]],1966=>[[186,3968,4435,248,4437]],1967=>[[367],[0]],1969=>[[3967,203]],1973=>[[3974],[0]],1975=>[[3976],[0]],1972=>[[21,3559],[171,3559,3850,3973,3975]],1974=>[[542,3559]],1976=>[[160,3559]],1978=>[[4369,4007,3977]],1979=>[[3980],[4046]],1980=>[[3981]],1981=>[[62,3854]],1983=>[[4023,730]],2034=>[[4006],[0]],1993=>[[3994,4000,4073,4001],[205,3995,4002,4073,3988],[523,3995,4002,4073,3991],[4034,4004]],1994=>[[265],[236]],1996=>[[420,265],[609,3995]],1997=>[[3983]],2003=>[[3999],[0]],1999=>[[3997]],2004=>[[3996,4000,4073,4001],[199,265,4002,4061,4046],[3981,4003]],2005=>[[4435],[0]],2006=>[[86,4005]],2007=>[[4117,4022]],2018=>[[4009],[0]],2009=>[[209,12]],2019=>[[4011],[0]],2011=>[[637],[554]],2021=>[[4027,4021],[4027],[0]],2013=>[[4021]],2014=>[[4042,4014],[4042],[0]],2015=>[[4014]],2016=>[[4013],[4015]],2020=>[[4282,4018,17,3854,4019,4016]],2022=>[[4020],[4021]],2041=>[[420],[0]],2039=>[[265],[0]],2027=>[[4023,4479],[4028],[128,4031],[4032],[383,614,372,4030],[24],[501,128,627],[4041,265],[609,4039],[75,4469],[4281],[74,4037],[553,4038],[4033],[4035],[4036]],2028=>[[371,720]],2029=>[[3854]],2031=>[[4458],[372,4030],[4029]],2032=>[[4082]],2033=>[[707,4453]],2035=>[[4034,3981]],2036=>[[3983]],2037=>[[192],[152],[128]],2038=>[[142],[334],[128]],2042=>[[609,4039],[75,4465],[4040,376],[4041,265]],2043=>[[4440],[0]],2048=>[[4049],[0]],2056=>[[4057],[0]],2046=>[[443,4414,4043,4048,4056]],2047=>[[204],[402],[513]],2049=>[[320,4047]],2054=>[[4051],[0]],2051=>[[383,133,4058]],2055=>[[4053],[0]],2053=>[[383,614,4058]],2057=>[[383,614,4058,4054],[383,133,4058,4055]],2058=>[[4059],[506,4479],[373,3],[506,128]],2059=>[[471],[49]],2062=>[[4063,4062],[4063],[0]],2061=>[[753,4066,4062,748]],2063=>[[750,4066]],2066=>[[4435,4111,4071]],2069=>[[4070,4069],[4070],[0]],2068=>[[753,4072,4069,748]],2070=>[[750,4072]],2072=>[[4066],[3854,4071]],2073=>[[4074],[4075]],2074=>[[4068]],2075=>[[4061]],2076=>[[4077]],2077=>[[44],[488],[220]],2078=>[[4080],[4083]],2080=>[[264,4262,4450],[75,4469],[4081]],2081=>[[4082]],2082=>[[662],[661]],2083=>[[4084,4076]],2084=>[[621],[599]],2085=>[[4080],[645,401,4435]],2086=>[[4080]],2087=>[[4117,-1]],2103=>[[4136],[0]],2090=>[[4483],[0]],2146=>[[32],[0]],2126=>[[4127],[0]],2117=>[[4118,4111,4103],[4120,4090,4103],[4121,4092,4103],[37,4111],[4122],[4123,4133,4116],[60,4111,4116],[32,4111],[4124,4133,4146],[4130,4111,4146],[628,4133],[656,4111,4103],[116],[586,4106],[583,4106],[113,4106],[587],[39,4111],[4125],[293,628],[293,4126,4116],[589,4116],[580,4111,4116],[332,4116],[291,4116],[164,4460,4116],[506,4460,4116],[501],[4128],[4129]],2118=>[[249],[588],[516],[331],[31]],2131=>[[416],[0]],2120=>[[437],[146,4131]],2121=>[[195],[126],[378],[192]],2122=>[[42],[41]],2123=>[[60,633],[629]],2124=>[[358,629],[379],[361,629],[358,60,633],[361,633]],2125=>[[330],[290]],2127=>[[60,633],[629]],2128=>[[262]],2129=>[[212],[211],[411],[352],[279],[351],[412],[353]],2130=>[[361],[358,60]],2132=>[[437],[146,4131]],2133=>[[753,4134,748]],2134=>[[4453],[783]],2137=>[[4138,4137],[4138]],2136=>[[4137]],2138=>[[512],[612],[657]],2142=>[[4143],[0]],2141=>[[4145],[4147],[46],[3847,4150,4146],[32,4142]],2143=>[[3847,4150]],2145=>[[19,4146],[32,19]],2147=>[[606,4146],[32,606]],2148=>[[753,4451,748]],2149=>[[753,787,748]],2150=>[[4484],[32],[4151]],2151=>[[128]],2152=>[[4484],[4153],[4154]],2153=>[[128]],2154=>[[32]],2158=>[[4159,4158],[4159],[0]],2156=>[[4181,4158]],2159=>[[4157,4181]],2160=>[[4181,4160],[4181]],2161=>[[4160]],2175=>[[4416],[0]],2181=>[[163,4262,4404],[4184],[323,4262,4452],[344,4262,4452],[25,4262,4450],[406,4262,4463],[75,4262,4463],[4186],[4188],[24,4262,4452],[399,4262,4203],[4189,4262,4203],[4190,4262,4450],[132,4262,4450],[486,4262,4191],[608,4262,753,4175,748],[4212],[4206],[243,4262,4192],[112,139,4262,4465],[236,139,4262,4465],[572,4195,4435],[553,4196],[84,4262,4465],[264,4262,4450],[4197],[4199],[4201],[4202]],2182=>[[376],[4484]],2184=>[[721,4183,4182]],2186=>[[81,4262,4465]],2188=>[[158,4262,4465]],2189=>[[544],[545],[546]],2190=>[[61],[575]],2191=>[[128],[152],[192],[80],[442],[78]],2192=>[[373],[191],[268]],2194=>[[4262]],2195=>[[4194],[0]],2196=>[[142],[334]],2197=>[[543,592]],2199=>[[848,4262,4465]],2201=>[[849,4262,4465]],2202=>[[2350]],2203=>[[4450],[128]],2210=>[[128],[0]],2206=>[[4210,69,4262,4152]],2209=>[[4210,158,4262,4463]],2212=>[[4210,3847,4262,4150]],2217=>[[4218],[0]],2214=>[[4229],[0]],2215=>[[4237],[0]],2216=>[[405,45,4223,4217,4214,4215]],2218=>[[404,4451]],2227=>[[277],[0]],2230=>[[4234],[0]],2225=>[[4437],[0]],2223=>[[4227,265,4230,753,4225,748],[4227,220,753,3589,748],[4224,4226]],2224=>[[432],[280]],2226=>[[753,3589,748],[71,753,4225,748]],2232=>[[4233],[0]],2229=>[[561,45,4227,4231,4232]],2231=>[[220,753,3589,748],[265,4230,4440]],2233=>[[560,4451]],2234=>[[4235]],2235=>[[9,763,4451]],2238=>[[4239,4238],[4239],[0]],2237=>[[753,4243,4238,748]],2239=>[[750,4243]],2245=>[[4246],[0]],2266=>[[4263,4266],[4263],[0]],2250=>[[4251],[0]],2243=>[[405,4435,4245,4266,4250]],2244=>[[4269],[329]],2246=>[[626,273,581,4244],[626,251,4253]],2249=>[[4248,4249],[4248],[0]],2248=>[[750,4267]],2251=>[[753,4267,4249,748]],2254=>[[4255,4254],[4255],[0]],2253=>[[4269],[753,4269,4254,748]],2255=>[[750,4269]],2263=>[[572,4262,4435],[4257,163,4262,4404],[368,4262,4451],[4264,4262,4451],[4265,139,4262,4469],[75,4262,4469]],2264=>[[323],[344]],2265=>[[112],[236]],2267=>[[561,4484,4266]],2270=>[[4271,4270],[4271],[0]],2269=>[[753,4272,4270,748]],2271=>[[750,4272]],2272=>[[3589],[329]],2273=>[[130,763,4360]],2274=>[[231,174]],2275=>[[231,3848,174]],2278=>[[4279],[0]],2277=>[[4278,4280]],2279=>[[251],[397],[240]],2280=>[[4423,4283]],2281=>[[69,4152]],2283=>[[4117,4282]],2284=>[[753,4386,750,4386,748]],2287=>[[4288,4287],[4288],[0]],2286=>[[4394,4287]],2288=>[[750,4394]],2291=>[[4292,4291],[4292],[0]],2290=>[[4293,4291]],2292=>[[750,4293]],2293=>[[4377,4489,4294]],2294=>[[3559],[128]],2295=>[[3847,4150]],2296=>[[4299,4296],[4299]],2297=>[[71,4296]],2298=>[[392],[0]],2299=>[[579,45,4465],[4298,157,45,4465],[167,45,4465]],2300=>[[4302,4300],[4302]],2301=>[[278,4300]],2302=>[[4303,45,4465]],2303=>[[579],[541]],2306=>[[4307,4306],[4307],[0]],2305=>[[4360,4306]],2307=>[[750,4360]],2310=>[[4311,4310],[4311],[0]],2309=>[[4317,4310]],2311=>[[750,4317]],2314=>[[4315,4314],[4315],[0]],2313=>[[4333,4314]],2315=>[[750,4333]],2331=>[[4332],[0]],2317=>[[4360,4331]],2318=>[[406]],2328=>[[4320],[0]],2320=>[[4318]],2321=>[[45,4465]],2329=>[[4323],[0]],2323=>[[17,4466],[4321]],2326=>[[4325],[0]],2325=>[[645,4484]],2327=>[[4326,45,734,406]],2330=>[[45,4328,4465],[645,4484,4329],[4327]],2332=>[[230,4330]],2333=>[[4334,4350]],2334=>[[3118],[4360]],2342=>[[4336],[0]],2336=>[[645,4484]],2337=>[[734,406]],2338=>[[4337],[4465]],2345=>[[4341],[0]],2341=>[[17,4466,4344]],2349=>[[4347],[0]],2347=>[[4342,45,4338,4343,4344],[645,4484,4345]],2348=>[[4352]],2350=>[[230,4349],[4348],[0]],2351=>[[727,101,406]],2352=>[[141,728,406]],2353=>[[458,4465]],2357=>[[4358],[0]],2355=>[[4484,4357]],2356=>[[4484],[0]],2358=>[[746,4356],[792]],2360=>[[4355],[105,4359]],2361=>[[275,4463]],2362=>[[4361],[2765]],2363=>[[385],[380]],2364=>[[284],[375]],2365=>[[4366]],2366=>[[405,4440]],2368=>[[4449],[4442,4445]],2369=>[[4370],[4371]],2370=>[[4435]],2371=>[[4368]],2372=>[[4435]],2375=>[[4376,4375],[4376],[0]],2374=>[[753,4372,4375,748]],2376=>[[750,4372]],2377=>[[4368]],2378=>[[4377],[4382]],2379=>[[4435]],2380=>[[4368]],2383=>[[4384],[0]],2382=>[[4435,751,4383,775]],2384=>[[4435,751]],2385=>[[4435]],2386=>[[4435]],2387=>[[4442]],2388=>[[4442]],2389=>[[4442]],2390=>[[4442]],2391=>[[4442]],2392=>[[4442]],2393=>[[4442],[4449]],2394=>[[4442],[4449]],2395=>[[4435]],2396=>[[4435]],2397=>[[4435]],2398=>[[4435]],2399=>[[4442]],2400=>[[4442]],2401=>[[4435]],2402=>[[4484]],2403=>[[4484]],2404=>[[4484]],2405=>[[4442],[4449]],2406=>[[4386,4449]],2412=>[[4413],[0]],2408=>[[4435,4412]],2411=>[[4410],[0]],2410=>[[751,775]],2413=>[[751,775],[4449,4411]],2414=>[[4442],[4449]],2417=>[[4418,4417],[4418],[0]],2416=>[[4414,4417]],2418=>[[750,4414]],2421=>[[4422,4421],[4422],[0]],2420=>[[4408,4421]],2422=>[[750,4408]],2423=>[[4435]],2424=>[[4432],[4501]],2425=>[[4424]],2426=>[[4432],[4510]],2427=>[[4426]],2428=>[[4435]],2429=>[[4463]],2430=>[[4435]],2431=>[[4435]],2432=>[[4433],[4434]],2433=>[[793],[781]],2434=>[[784]],2435=>[[4432],[4493]],2438=>[[4439,4438],[4439],[0]],2437=>[[4435,4438]],2439=>[[750,4435]],2440=>[[753,4437,748]],2442=>[[4435,4445]],2446=>[[4447],[0]],2444=>[[4435,4446],[4448]],2447=>[[4449,4445]],2448=>[[4449,4449]],2449=>[[751,4435]],2450=>[[787],[786],[788],[791],[783],[785]],2451=>[[787],[786],[788],[791]],2452=>[[787],[788],[791],[783],[785]],2453=>[[787],[4454],[791],[788]],2454=>[[786]],2470=>[[794],[0]],2456=>[[4469],[4477],[4480],[4479],[4478],[4470,4457]],2457=>[[786],[782]],2458=>[[4456],[778,4450],[773,4450]],2461=>[[4462,4461],[4462],[0]],2460=>[[753,4465,4461,748]],2462=>[[750,4465]],2463=>[[790],[4464]],2464=>[[784]],2465=>[[4463],[786],[782]],2466=>[[4463],[4467]],2467=>[[786]],2468=>[[4463,4468],[4463],[0]],2469=>[[4471,4468]],2471=>[[4470,4463],[789]],2472=>[[4463]],2475=>[[4476,4475],[4476],[0]],2474=>[[4463,4475]],2476=>[[750,4463]],2477=>[[787],[788],[791],[783],[785]],2478=>[[596],[183]],2479=>[[376],[801]],2480=>[[116,4463],[586,4463],[583,4463]],2481=>[[4133],[4483]],2482=>[[4133]],2483=>[[753,787,750,787,748]],2484=>[[4435],[4463]],2485=>[[4432],[4514]],2486=>[[4426],[4463]],2487=>[[4453],[4432]],2488=>[[753,748]],2489=>[[763],[756]],2490=>[[658],[673],[214],[284],[502]],2491=>[[214,751],[284,751],[502,751]],2492=>[[658,751],[673,751],[214,751],[284,751],[502,751]],2493=>[[4497],[4498]],2494=>[[510]],2495=>[[714]],2496=>[[4501],[4516],[173],[4494],[4495]],2497=>[[4496]],2498=>[[4506],[4499],[4500],[4505],[4515]],2499=>[[173],[714],[510]],2500=>[[19],[29],[46],[47],[58],[61],[677],[75],[77],[90],[123],[147],[159],[196],[197],[219],[222],[234],[245],[267],[373],[415],[417],[455],[468],[480],[489],[512],[514],[543],[552],[597],[606],[607],[651]],2501=>[[4503],[4504]],2502=>[[4520],[170],[188],[369],[423],[427],[451],[459],[709],[565]],2503=>[[4502]],2504=>[[4506],[4505],[4515]],2505=>[[170],[188],[369],[423],[427],[451],[459],[709],[565]],2506=>[[4507],[4509]],2507=>[[3],[2],[724],[5],[660],[6],[7],[8],[9],[12],[16],[21],[812],[23],[24],[25],[26],[27],[33],[37],[40],[41],[42],[44],[675],[50],[53],[54],[56],[57],[63],[64],[65],[66],[67],[68],[70],[71],[74],[73],[76],[78],[79],[664],[80],[81],[82],[84],[85],[87],[88],[89],[91],[96],[101],[107],[111],[112],[113],[116],[122],[129],[130],[715],[132],[716],[138],[139],[140],[141],[142],[150],[151],[152],[156],[158],[160],[730],[162],[163],[848],[164],[166],[165],[168],[169],[171],[172],[680],[176],[177],[179],[180],[181],[184],[185],[189],[190],[191],[192],[682],[201],[202],[204],[208],[211],[212],[213],[713],[840],[216],[210],[841],[220],[674],[705],[225],[224],[229],[230],[233],[725],[235],[238],[844],[243],[244],[661],[250],[255],[256],[258],[259],[262],[264],[847],[268],[270],[273],[274],[279],[280],[670],[286],[288],[289],[296],[735],[298],[299],[319],[300],[729],[301],[302],[303],[304],[712],[305],[306],[307],[308],[309],[310],[312],[311],[313],[314],[316],[738],[317],[318],[736],[321],[322],[323],[324],[327],[328],[333],[334],[335],[336],[337],[340],[343],[344],[346],[348],[350],[351],[352],[353],[354],[355],[356],[357],[358],[361],[363],[702],[365],[366],[367],[368],[671],[374],[689],[377],[379],[381],[732],[728],[384],[386],[387],[719],[390],[703],[717],[690],[398],[399],[400],[401],[402],[403],[404],[406],[704],[407],[408],[409],[410],[411],[412],[413],[693],[418],[419],[421],[737],[424],[426],[425],[429],[430],[431],[434],[438],[439],[441],[846],[442],[718],[445],[446],[447],[448],[449],[452],[842],[454],[456],[460],[462],[461],[463],[466],[464],[465],[617],[695],[470],[472],[727],[473],[851],[474],[706],[476],[659],[481],[482],[483],[485],[486],[488],[490],[492],[721],[849],[722],[720],[723],[495],[496],[500],[501],[503],[508],[513],[669],[515],[517],[519],[520],[521],[813],[814],[815],[817],[816],[818],[819],[820],[821],[822],[823],[824],[825],[826],[829],[828],[830],[831],[833],[832],[834],[827],[835],[522],[836],[837],[838],[839],[528],[529],[530],[532],[535],[538],[707],[540],[542],[544],[545],[546],[547],[553],[556],[557],[558],[559],[560],[561],[566],[567],[568],[571],[572],[575],[576],[577],[578],[580],[581],[708],[697],[584],[585],[583],[586],[845],[592],[593],[598],[599],[698],[601],[602],[603],[604],[610],[613],[615],[618],[619],[625],[627],[631],[711],[636],[662],[638],[639],[640],[641],[646],[647],[648],[650],[652],[653],[656],[843]],2508=>[[731],[741],[735],[738],[736],[733],[744],[740],[737],[734],[739],[742],[743],[583],[586]],2509=>[[4508]],2510=>[[4512],[4513]],2511=>[[4520],[4516]],2512=>[[4511]],2513=>[[4506],[4500],[4515]],2514=>[[4506],[4499],[4500],[4505]],2515=>[[214],[284],[658],[673],[502]],2516=>[[4517],[4518],[4519]],2517=>[[2],[19],[12],[27],[29],[46],[47],[58],[61],[677],[66],[75],[77],[90],[123],[147],[159],[196],[197],[201],[210],[219],[222],[224],[245],[661],[267],[373],[387],[390],[398],[401],[413],[415],[417],[452],[455],[468],[470],[659],[480],[489],[720],[721],[722],[723],[496],[503],[512],[519],[514],[520],[543],[552],[597],[606],[607],[615],[662],[648],[651]],2518=>[[510]],2519=>[[234]],2520=>[[4521],[4522],[4524],[4526],[4527]],2521=>[[3],[724],[5],[6],[7],[8],[9],[13],[16],[21],[22],[24],[23],[25],[26],[33],[37],[40],[42],[41],[44],[675],[50],[53],[54],[56],[57],[63],[65],[64],[67],[68],[70],[73],[74],[71],[76],[78],[79],[664],[80],[81],[82],[84],[85],[87],[89],[88],[91],[93],[96],[101],[107],[112],[111],[113],[116],[122],[129],[130],[132],[136],[716],[138],[139],[140],[141],[142],[150],[151],[152],[158],[160],[164],[163],[162],[165],[166],[168],[169],[171],[680],[176],[179],[180],[181],[185],[184],[682],[202],[156],[204],[189],[190],[191],[192],[208],[212],[211],[213],[216],[214],[220],[674],[705],[225],[229],[230],[233],[250],[235],[238],[244],[725],[255],[256],[258],[259],[243],[262],[850],[264],[268],[270],[273],[274],[279],[280],[284],[670],[286],[288],[289],[323],[316],[319],[300],[304],[301],[302],[318],[303],[712],[306],[298],[305],[299],[314],[308],[307],[317],[309],[310],[311],[312],[313],[296],[321],[322],[325],[324],[327],[328],[333],[334],[335],[336],[337],[340],[343],[344],[348],[346],[350],[351],[352],[353],[354],[355],[357],[356],[358],[361],[363],[702],[365],[367],[366],[374],[368],[689],[671],[377],[379],[381],[728],[382],[384],[719],[703],[717],[690],[399],[400],[402],[403],[404],[406],[704],[407],[409],[410],[408],[411],[412],[693],[418],[419],[708],[421],[424],[425],[426],[429],[430],[431],[434],[438],[439],[441],[440],[442],[445],[446],[447],[448],[449],[676],[454],[456],[460],[461],[462],[463],[464],[465],[466],[617],[695],[472],[727],[473],[474],[706],[476],[481],[482],[483],[485],[486],[488],[490],[492],[495],[501],[500],[502],[508],[513],[669],[515],[517],[521],[522],[528],[529],[530],[533],[532],[535],[538],[707],[540],[542],[544],[545],[546],[547],[553],[556],[557],[558],[559],[561],[560],[565],[566],[567],[568],[576],[571],[575],[572],[577],[578],[580],[581],[697],[592],[593],[583],[584],[585],[586],[598],[599],[600],[698],[601],[602],[604],[603],[610],[613],[618],[619],[631],[711],[636],[627],[639],[638],[640],[647],[641],[650],[652],[653],[656]],2522=>[[510]],2523=>[[99],[234],[206],[484],[487]],2524=>[[4523]],2525=>[[172],[177],[386],[565],[625],[646]],2526=>[[4525]],2527=>[[660]]]];
+return ['rules_offset'=>2000,'rules_names'=>['query','%f1','%f2','%f3','simpleStatement','%f4','%f5','%f6','%f7','alterStatement','%f8','%f9','%f10','%f11','alterInstance','%f12','%f13','%f14','%f15','%f16','%f17','%f18','%f19','%f20','%f21','%f22','%f23','%f24','%f25','%f26','%f27','alterDatabase','%f28','%f29','%f30','%f31','%f32','%f33','%f34','%f35','%f36','%f37','alterEvent','%f38','%f39','%f40','%f41','%f42','%f43','%f44','%f45','%f46','%f47','%f48','%f49','%f50','%f51','%f52','%f53','%f54','alterLogfileGroup','%f55','alterLogfileGroupOptions','%f56','%f57','%f58','alterLogfileGroupOption','alterServer','%f59','%f60','%f61','alterTable','%f62','%f63','%f64','%f65','%f66','alterTableActions','%f67','%f68','%f69','%f70','%f71','alterCommandList','%f72','%f73','%f74','alterCommandsModifierList','%f75','%f76','standaloneAlterCommands','%f77','%f78','%f79','%f80','%f81','%f82','%f83','%f84','%f85','%f86','%f87','%f88','%f89','%f90','alterPartition','%f91','%f92','%f93','%f94','%f95','%f96','alterList','%f97','%f98','%f99','%f100','alterCommandsModifier','%f101','%f102','%f103','%f104','%f105','%f106','%f107','%f108','alterListItem','%f109','%f110','%f111','%f112','%f113','%f114','%f115','%f116','%f117','%f118','%f119','%f120','%f121','%f122','%f123','%f124','%f125','%f126','%f127','%f128','%f129','%f130','%f131','%f132','%f133','%f134','place','restrict','%f135','%f136','alterOrderList','%f137','%f138','%f139','%f140','alterAlgorithmOption','%f141','%f142','alterLockOption','%f143','%f144','%f145','indexLockAndAlgorithm','withValidation','%f146','%f147','removePartitioning','allOrPartitionNameList','alterTablespace','%f148','%f149','%f150','%f151','%f152','%f153','%f154','%f155','%f156','%f157','%f158','%f159','%f160','%f161','%f162','alterUndoTablespace','%f163','%f164','undoTableSpaceOptions','%f165','%f166','%f167','undoTableSpaceOption','%f168','alterTablespaceOptions','%f169','%f170','%f171','%f172','alterTablespaceOption','%f173','%f174','changeTablespaceOption','%f175','%f176','%f177','alterView','%f178','viewTail','%f179','viewSelect','%f180','viewCheckOption','%f181','%f182','createStatement','%f183','%f184','%f185','%f186','%f187','%f188','createDatabase','createDatabaseOption','%f189','%f190','%f191','createTable','%f192','%f193','%f194','%f195','%f196','%f197','%f198','%f199','tableElementList','%f200','%f201','tableElement','%f202','%f203','duplicateAsQueryExpression','%f204','%f205','queryExpressionOrParens','%f206','createRoutine','%f207','%f208','%f209','%f210','createProcedure','%f211','%f212','%f213','%f214','%f215','%f216','%f217','%f218','%f219','%f220','%f221','createFunction','%f222','%f223','%f224','%f225','%f226','%f227','%f228','%f229','%f230','createUdf','%f231','%f232','routineCreateOption','%f233','routineAlterOptions','routineOption','%f234','%f235','%f236','createIndex','%f237','%f238','%f239','%f240','%f241','%f242','%f243','%f244','%f245','%f246','%f247','indexNameAndType','%f248','%f249','%f250','createIndexTarget','%f251','createLogfileGroup','%f252','%f253','logfileGroupOptions','%f254','%f255','%f256','logfileGroupOption','createServer','%f257','serverOptions','%f258','%f259','serverOption','%f260','%f261','createTablespace','%f262','%f263','%f264','createUndoTablespace','tsDataFileName','%f265','%f266','%f267','%f268','tsDataFile','%f269','tablespaceOptions','%f270','%f271','%f272','tablespaceOption','%f273','%f274','%f275','%f276','tsOptionInitialSize','%f277','tsOptionUndoRedoBufferSize','%f278','%f279','tsOptionAutoextendSize','%f280','tsOptionMaxSize','%f281','tsOptionExtentSize','%f282','tsOptionNodegroup','%f283','%f284','tsOptionEngine','%f285','tsOptionEngineAttribute','tsOptionWait','%f286','%f287','tsOptionComment','%f288','tsOptionFileblockSize','%f289','tsOptionEncryption','%f290','%f291','%f292','createView','%f293','viewReplaceOrAlgorithm','viewAlgorithm','%f294','viewSuid','%f295','%f296','%f297','createTrigger','%f298','%f299','%f300','%f301','%f302','triggerFollowsPrecedesClause','%f303','%f304','%f305','%f306','%f307','%f308','%f309','createEvent','%f310','%f311','%f312','%f313','%f314','%f315','%f316','%f317','%f318','%f319','%f320','createRole','%f321','%f322','%f323','createSpatialReference','srsAttribute','dropStatement','%f324','%f325','%f326','%f327','%f328','dropDatabase','%f329','dropEvent','%f330','dropFunction','%f331','dropProcedure','%f332','%f333','dropIndex','%f334','dropLogfileGroup','%f335','%f336','%f337','%f338','%f339','%f340','dropLogfileGroupOption','%f341','dropServer','%f342','%f343','%f344','dropTable','%f345','%f346','%f347','%f348','dropTableSpace','%f349','%f350','%f351','%f352','%f353','%f354','%f355','dropTrigger','%f356','%f357','dropView','%f358','%f359','%f360','dropRole','%f361','dropSpatialReference','%f362','dropUndoTablespace','%f363','renameTableStatement','%f364','%f365','%f366','renamePair','%f367','truncateTableStatement','importStatement','%f368','callStatement','%f369','%f370','%f371','%f372','%f373','deleteStatement','%f374','%f375','%f376','%f377','%f378','%f379','%f380','%f381','%f382','%f383','%f384','%f385','%f386','%f387','%f388','partitionDelete','%f389','deleteStatementOption','doStatement','%f390','%f391','%f392','handlerStatement','%f393','%f394','%f395','%f396','%f397','handlerReadOrScan','%f398','%f399','%f400','%f401','%f402','%f403','%f404','%f405','%f406','insertStatement','%f407','%f408','%f409','%f410','%f411','%f412','%f413','%f414','%f415','insertLockOption','%f416','insertFromConstructor','%f417','%f418','%f419','%f420','fields','%f421','%f422','insertValues','%f423','%f424','insertQueryExpression','%f425','%f426','valueList','%f427','%f428','%f429','%f430','values','%f431','%f432','%f433','%f434','%f435','valuesReference','insertUpdateList','%f436','%f437','%f438','%f439','%f440','%f441','%f442','%f443','loadStatement','%f444','%f445','%f446','%f447','dataOrXml','xmlRowsIdentifiedBy','%f448','%f449','%f450','loadDataFileTail','%f451','%f452','%f453','%f454','%f455','%f456','loadDataFileTargetList','%f457','fieldOrVariableList','%f458','%f459','%f460','%f461','%f462','%f463','%f464','replaceStatement','%f465','%f466','%f467','%f468','selectStatement','%f469','selectStatementWithInto','%f470','%f471','queryExpression','%f472','%f473','%f474','%f475','%f476','%f477','%f478','%f479','%f480','%f481','%f482','%f483','queryExpressionBody','%f484','%f485','%f486','%f487','%f488','%f489','queryTerm','%f490','%f491','%f492','%f493','%f494','%f495','%f496','queryExpressionParens','queryPrimary','%f497','%f498','%f499','%f500','%f501','%f502','%f503','%f504','%f505','querySpecification','%f506','%f507','%f508','subquery','querySpecOption','limitClause','simpleLimitClause','%f509','limitOptions','%f510','%f511','%f512','limitOption','%f513','intoClause','%f514','%f515','%f516','%f517','%f518','%f519','%f520','%f521','%f522','%f523','procedureAnalyseClause','%f524','%f525','%f526','%f527','%f528','havingClause','%f529','windowClause','%f530','%f531','windowDefinition','windowSpec','%f532','%f533','%f534','%f535','%f536','%f537','%f538','%f539','%f540','%f541','windowSpecDetails','%f542','%f543','%f544','%f545','%f546','%f547','%f548','windowFrameClause','windowFrameUnits','windowFrameExtent','windowFrameStart','windowFrameBetween','windowFrameBound','windowFrameExclusion','%f549','%f550','%f551','withClause','%f552','%f553','%f554','commonTableExpression','%f555','groupByClause','olapOption','%f556','orderClause','direction','fromClause','%f557','%f558','tableReferenceList','%f559','%f560','%f561','tableValueConstructor','%f562','%f563','explicitTable','%f564','rowValueExplicit','selectOption','%f565','%f566','%f567','lockingClauseList','%f568','%f569','lockingClause','%f570','%f571','%f572','%f573','%f574','%f575','lockStrengh','%f576','lockedRowAction','%f577','selectItemList','%f578','%f579','%f580','%f581','selectItem','%f582','selectAlias','%f583','whereClause','%f584','tableReference','%f585','%f586','%f587','%f588','escapedTableReference','%f589','joinedTable','%f590','%f591','%f592','%f593','%f594','naturalJoinType','%f595','%f596','innerJoinType','%f597','%f598','%f599','outerJoinType','%f600','tableFactor','%f601','%f602','%f603','%f604','singleTable','singleTableParens','%f605','%f606','%f607','derivedTable','%f608','%f609','%f610','%f611','%f612','%f613','tableReferenceListParens','%f614','%f615','tableFunction','%f616','columnsClause','%f617','%f618','%f619','%f620','%f621','jtColumn','%f622','%f623','%f624','%f625','%f626','onEmptyOrError','onEmpty','onError','jtOnResponse','setOperationOption','%f627','tableAlias','%f628','%f629','%f630','%f631','indexHintList','%f632','%f633','%f634','indexHint','indexHintType','keyOrIndex','%f635','constraintKeyType','indexHintClause','%f636','%f637','indexList','%f638','%f639','indexListElement','%f640','%f641','%f642','%f643','%f644','%f645','updateStatement','%f646','%f647','%f648','transactionOrLockingStatement','%f649','%f650','%f651','%f652','transactionStatement','%f653','%f654','%f655','%f656','%f657','%f658','%f659','beginWork','%f660','transactionCharacteristicList','%f661','%f662','transactionCharacteristic','%f663','%f664','%f665','savepointStatement','%f666','%f667','%f668','%f669','%f670','%f671','%f672','%f673','%f674','%f675','%f676','lockStatement','%f677','%f678','%f679','%f680','%f681','%f682','%f683','lockItem','%f684','%f685','lockOption','xaStatement','%f686','%f687','%f688','%f689','%f690','%f691','%f692','%f693','%f694','%f695','%f696','%f697','%f698','%f699','xaConvert','%f700','%f701','%f702','%f703','%f704','xid','%f705','%f706','%f707','%f708','%f709','%f710','replicationStatement','%f711','%f712','%f713','%f714','%f715','%f716','%f717','%f718','%f719','%f720','%f721','%f722','%f723','%f724','resetOption','%f725','masterResetOptions','%f726','%f727','%f728','%f729','replicationLoad','%f730','%f731','changeMaster','%f732','changeMasterOptions','%f733','%f734','masterOption','privilegeCheckDef','tablePrimaryKeyCheckDef','masterTlsCiphersuitesDef','masterFileDef','%f735','serverIdList','%f736','%f737','%f738','%f739','%f740','%f741','%f742','%f743','changeReplication','%f744','%f745','%f746','%f747','%f748','%f749','changeReplicationSourceOptions','%f750','%f751','replicationSourceOption','%f752','%f753','%f754','%f755','%f756','%f757','%f758','%f759','%f760','%f761','%f762','%f763','%f764','%f765','%f766','%f767','%f768','%f769','%f770','%f771','%f772','%f773','%f774','%f775','%f776','%f777','%f778','%f779','%f780','%f781','%f782','%f783','%f784','%f785','filterDefinition','%f786','filterDbList','%f787','%f788','%f789','filterTableList','%f790','%f791','%f792','filterStringList','%f793','%f794','filterWildDbTableString','%f795','filterDbPairList','%f796','%f797','%f798','%f799','%f800','%f801','%f802','slave','%f803','%f804','%f805','slaveUntilOptions','%f806','%f807','%f808','%f809','%f810','%f811','slaveConnectionOptions','%f812','%f813','%f814','%f815','%f816','%f817','%f818','%f819','%f820','%f821','%f822','%f823','%f824','%f825','slaveThreadOptions','%f826','%f827','slaveThreadOption','groupReplication','%f828','preparedStatement','%f829','%f830','%f831','executeStatement','%f832','%f833','%f834','executeVarList','%f835','%f836','cloneStatement','%f837','%f838','%f839','%f840','%f841','%f842','%f843','%f844','%f845','dataDirSSL','%f846','ssl','accountManagementStatement','%f847','%f848','%f849','alterUser','%f850','%f851','%f852','alterUserTail','%f853','%f854','%f855','%f856','%f857','%f858','userFunction','createUser','%f859','%f860','createUserTail','%f861','%f862','%f863','%f864','%f865','%f866','%f867','%f868','%f869','defaultRoleClause','%f870','%f871','%f872','%f873','requireClause','%f874','%f875','%f876','connectOptions','%f877','%f878','accountLockPasswordExpireOptions','%f879','%f880','%f881','%f882','%f883','%f884','%f885','%f886','%f887','%f888','%f889','%f890','%f891','dropUser','%f892','%f893','%f894','grant','%f895','%f896','%f897','%f898','%f899','%f900','%f901','%f902','%f903','%f904','%f905','%f906','%f907','%f908','grantTargetList','%f909','%f910','grantOptions','%f911','%f912','%f913','exceptRoleList','withRoles','%f914','%f915','%f916','grantAs','versionedRequireClause','%f917','%f918','renameUser','%f919','%f920','%f921','%f922','revoke','%f923','%f924','%f925','%f926','%f927','%f928','%f929','%f930','%f931','%f932','%f933','%f934','%f935','%f936','%f937','%f938','onTypeTo','%f939','%f940','%f941','%f942','%f943','%f944','%f945','aclType','%f946','roleOrPrivilegesList','%f947','%f948','%f949','%f950','%f951','roleOrPrivilege','%f952','%f953','%f954','%f955','%f956','%f957','%f958','%f959','%f960','%f961','%f962','%f963','%f964','grantIdentifier','%f965','%f966','%f967','%f968','%f969','requireList','%f970','%f971','%f972','requireListElement','grantOption','%f973','setRole','%f974','%f975','%f976','%f977','%f978','roleList','%f979','%f980','%f981','role','%f982','%f983','%f984','%f985','%f986','%f987','%f988','%f989','%f990','tableAdministrationStatement','%f991','%f992','%f993','%f994','%f995','%f996','%f997','%f998','%f999','%f1000','%f1001','%f1002','histogram','%f1003','%f1004','%f1005','%f1006','checkOption','%f1007','repairType','%f1008','%f1009','installUninstallStatment','%f1010','%f1011','%f1012','%f1013','installOptionType','%f1014','installSetValue','%f1015','%f1016','installSetValueList','%f1017','%f1018','setStatement','%f1019','startOptionValueList','%f1020','%f1021','%f1022','%f1023','%f1024','%f1025','%f1026','%f1027','%f1028','%f1029','%f1030','%f1031','%f1032','%f1033','%f1034','%f1035','%f1036','transactionCharacteristics','%f1037','%f1038','%f1039','%f1040','transactionAccessMode','%f1041','isolationLevel','%f1042','%f1043','%f1044','optionValueListContinued','%f1045','%f1046','optionValueNoOptionType','%f1047','%f1048','%f1049','optionValue','%f1050','setSystemVariable','startOptionValueListFollowingOptionType','optionValueFollowingOptionType','setExprOrDefault','%f1051','%f1052','%f1053','showStatement','%f1054','%f1055','%f1056','%f1057','%f1058','%f1059','%f1060','%f1061','%f1062','%f1063','%f1064','%f1065','%f1066','%f1067','%f1068','%f1069','%f1070','%f1071','%f1072','%f1073','%f1074','%f1075','%f1076','%f1077','%f1078','%f1079','%f1080','%f1081','%f1082','%f1083','%f1084','%f1085','%f1086','%f1087','%f1088','%f1089','%f1090','%f1091','%f1092','%f1093','%f1094','%f1095','%f1096','%f1097','%f1098','%f1099','%f1100','%f1101','%f1102','%f1103','%f1104','%f1105','%f1106','%f1107','%f1108','%f1109','%f1110','%f1111','%f1112','%f1113','%f1114','%f1115','%f1116','%f1117','%f1118','%f1119','%f1120','%f1121','%f1122','showCommandType','%f1123','%f1124','nonBlocking','%f1125','%f1126','fromOrIn','inDb','profileType','%f1127','%f1128','%f1129','otherAdministrativeStatement','%f1130','%f1131','%f1132','%f1133','%f1134','%f1135','%f1136','%f1137','keyCacheListOrParts','%f1138','keyCacheList','%f1139','%f1140','%f1141','assignToKeycache','%f1142','assignToKeycachePartition','%f1143','cacheKeyList','keyUsageElement','%f1144','keyUsageList','%f1145','%f1146','%f1147','%f1148','flushOption','%f1149','%f1150','%f1151','logType','%f1152','flushTables','%f1153','%f1154','%f1155','%f1156','flushTablesOptions','%f1157','%f1158','%f1159','preloadTail','%f1160','%f1161','%f1162','preloadList','%f1163','%f1164','%f1165','%f1166','preloadKeys','%f1167','%f1168','adminPartition','resourceGroupManagement','%f1169','%f1170','%f1171','%f1172','createResourceGroup','%f1173','%f1174','%f1175','resourceGroupVcpuList','%f1176','%f1177','%f1178','%f1179','vcpuNumOrRange','%f1180','%f1181','%f1182','resourceGroupPriority','resourceGroupEnableDisable','%f1183','%f1184','%f1185','%f1186','alterResourceGroup','%f1187','setResourceGroup','%f1188','%f1189','%f1190','threadIdList','%f1191','%f1192','%f1193','%f1194','dropResourceGroup','utilityStatement','%f1195','%f1196','describeStatement','%f1197','%f1198','%f1199','%f1200','explainStatement','%f1201','%f1202','%f1203','%f1204','%f1205','%f1206','%f1207','%f1208','explainableStatement','%f1209','%f1210','%f1211','helpCommand','useCommand','restartServer','%f1212','expr','%f1213','%f1214','%f1215','%f1216','%f1217','%f1218','%f1219','%f1220','%f1221','%f1222','%f1223','boolPri','%f1224','%f1225','%f1226','compOp','%f1227','predicate','%f1228','%f1229','%f1230','%f1231','%f1232','%f1233','predicateOperations','%f1234','%f1235','%f1236','%f1237','bitExpr','%f1238','%f1239','%f1240','%f1241','%f1242','%f1243','simpleExpr','%f1244','%f1245','%f1246','%f1247','%f1248','%f1249','%f1250','%f1251','%f1252','%f1253','%f1254','%f1255','%f1256','%f1257','%f1258','%f1259','%f1260','%f1261','%f1262','%f1263','%f1264','%f1265','%f1266','%f1267','%f1268','%f1269','%f1270','arrayCast','%f1271','jsonOperator','%f1272','%f1273','%f1274','%f1275','%f1276','%f1277','%f1278','%f1279','%f1280','%f1281','%f1282','%f1283','%f1284','%f1285','%f1286','%f1287','%f1288','%f1289','%f1290','%f1291','%f1292','%f1293','sumExpr','%f1294','%f1295','%f1296','%f1297','%f1298','%f1299','%f1300','%f1301','%f1302','%f1303','%f1304','%f1305','%f1306','%f1307','%f1308','%f1309','%f1310','%f1311','%f1312','%f1313','%f1314','%f1315','%f1316','%f1317','%f1318','%f1319','%f1320','%f1321','%f1322','%f1323','%f1324','%f1325','%f1326','%f1327','%f1328','%f1329','%f1330','%f1331','%f1332','%f1333','%f1334','%f1335','groupingOperation','%f1336','%f1337','%f1338','%f1339','%f1340','windowFunctionCall','%f1341','%f1342','%f1343','%f1344','%f1345','%f1346','windowingClause','%f1347','%f1348','leadLagInfo','%f1349','%f1350','%f1351','nullTreatment','%f1352','%f1353','%f1354','jsonFunction','%f1355','inSumExpr','identListArg','%f1356','identList','%f1357','%f1358','%f1359','fulltextOptions','%f1360','%f1361','%f1362','%f1363','%f1364','%f1365','%f1366','%f1367','%f1368','%f1369','%f1370','%f1371','%f1372','%f1373','%f1374','runtimeFunctionCall','%f1375','%f1376','%f1377','%f1378','%f1379','%f1380','%f1381','%f1382','%f1383','%f1384','%f1385','%f1386','%f1387','%f1388','%f1389','%f1390','%f1391','%f1392','%f1393','%f1394','%f1395','%f1396','%f1397','%f1398','%f1399','%f1400','%f1401','%f1402','%f1403','%f1404','geometryFunction','%f1405','%f1406','timeFunctionParameters','fractionalPrecision','%f1407','weightStringLevels','%f1408','%f1409','%f1410','%f1411','%f1412','weightStringLevelListItem','%f1413','%f1414','%f1415','%f1416','dateTimeTtype','trimFunction','%f1417','%f1418','%f1419','%f1420','%f1421','%f1422','%f1423','substringFunction','%f1424','%f1425','%f1426','%f1427','%f1428','%f1429','%f1430','%f1431','%f1432','functionCall','%f1433','udfExprList','%f1434','%f1435','%f1436','udfExpr','variable','userVariable','%f1437','%f1438','systemVariable','internalVariableName','%f1439','%f1440','%f1441','%f1442','%f1443','whenExpression','thenExpression','elseExpression','%f1444','%f1445','%f1446','%f1447','%f1448','%f1449','%f1450','%f1451','%f1452','castType','%f1453','%f1454','%f1455','%f1456','%f1457','exprList','%f1458','%f1459','charset','notRule','not2Rule','interval','%f1460','intervalTimeStamp','exprListWithParentheses','exprWithParentheses','simpleExprWithParentheses','%f1461','orderList','%f1462','%f1463','%f1464','orderExpression','%f1465','groupList','%f1466','%f1467','groupingExpression','channel','%f1468','compoundStatement','returnStatement','ifStatement','%f1469','ifBody','%f1470','%f1471','thenStatement','%f1472','compoundStatementList','%f1473','%f1474','%f1475','%f1476','%f1477','caseStatement','%f1478','%f1479','elseStatement','%f1480','labeledBlock','unlabeledBlock','label','%f1481','%f1482','beginEndBlock','%f1483','labeledControl','unlabeledControl','loopBlock','whileDoBlock','repeatUntilBlock','%f1484','spDeclarations','%f1485','%f1486','spDeclaration','%f1487','%f1488','variableDeclaration','%f1489','%f1490','conditionDeclaration','spCondition','%f1491','sqlstate','%f1492','handlerDeclaration','%f1493','%f1494','%f1495','handlerCondition','cursorDeclaration','iterateStatement','leaveStatement','%f1496','getDiagnostics','%f1497','%f1498','%f1499','%f1500','%f1501','%f1502','%f1503','%f1504','%f1505','%f1506','signalAllowedExpr','statementInformationItem','%f1507','%f1508','conditionInformationItem','%f1509','%f1510','signalInformationItemName','%f1511','signalStatement','%f1512','%f1513','%f1514','%f1515','%f1516','%f1517','%f1518','%f1519','resignalStatement','%f1520','%f1521','%f1522','%f1523','%f1524','%f1525','%f1526','signalInformationItem','cursorOpen','cursorClose','%f1527','cursorFetch','%f1528','%f1529','%f1530','%f1531','%f1532','schedule','%f1533','%f1534','%f1535','%f1536','%f1537','columnDefinition','checkOrReferences','%f1538','checkConstraint','%f1539','constraintEnforcement','%f1540','%f1541','%f1542','%f1543','%f1544','%f1545','%f1546','%f1547','%f1548','tableConstraintDef','%f1549','%f1550','%f1551','%f1552','%f1553','%f1554','%f1555','%f1556','%f1557','%f1558','%f1559','%f1560','constraintName','fieldDefinition','%f1561','%f1562','%f1563','%f1564','%f1565','%f1566','%f1567','%f1568','%f1569','%f1570','%f1571','%f1572','%f1573','%f1574','%f1575','%f1576','%f1577','%f1578','%f1579','columnAttribute','%f1580','%f1581','%f1582','%f1583','%f1584','%f1585','%f1586','%f1587','%f1588','columnFormat','storageMedia','%f1589','%f1590','%f1591','gcolAttribute','%f1592','%f1593','%f1594','references','%f1595','%f1596','%f1597','%f1598','%f1599','%f1600','%f1601','%f1602','%f1603','%f1604','%f1605','deleteOption','%f1606','%f1607','keyList','%f1608','%f1609','%f1610','%f1611','keyPart','%f1612','keyListWithExpression','%f1613','%f1614','%f1615','keyPartOrExpression','keyListVariants','%f1616','%f1617','indexType','%f1618','indexOption','%f1619','commonIndexOption','%f1620','visibility','indexTypeClause','%f1621','fulltextIndexOption','spatialIndexOption','dataTypeDefinition','%f1622','%f1623','%f1624','%f1625','%f1626','%f1627','%f1628','%f1629','%f1630','%f1631','%f1632','%f1633','%f1634','%f1635','%f1636','%f1637','%f1638','%f1639','%f1640','%f1641','%f1642','%f1643','%f1644','%f1645','%f1646','%f1647','%f1648','%f1649','%f1650','dataType','%f1651','%f1652','%f1653','%f1654','%f1655','%f1656','%f1657','%f1658','%f1659','%f1660','%f1661','%f1662','nchar','%f1663','realType','fieldLength','%f1664','%f1665','fieldOptions','%f1666','%f1667','%f1668','%f1669','charsetWithOptBinary','%f1670','%f1671','%f1672','ascii','%f1673','unicode','wsNumCodepoints','typeDatetimePrecision','charsetName','%f1674','collationName','%f1675','%f1676','%f1677','createTableOptions','%f1678','%f1679','%f1680','%f1681','createTableOptionsSpaceSeparated','%f1682','%f1683','%f1684','%f1685','%f1686','%f1687','%f1688','%f1689','%f1690','%f1691','%f1692','%f1693','%f1694','%f1695','%f1696','%f1697','%f1698','%f1699','%f1700','createTableOption','%f1701','%f1702','%f1703','%f1704','%f1705','%f1706','%f1707','%f1708','%f1709','%f1710','%f1711','%f1712','%f1713','%f1714','%f1715','%f1716','%f1717','%f1718','%f1719','%f1720','%f1721','ternaryOption','%f1722','%f1723','defaultCollation','%f1724','%f1725','defaultEncryption','%f1726','%f1727','defaultCharset','%f1728','%f1729','%f1730','partitionClause','%f1731','%f1732','%f1733','%f1734','%f1735','%f1736','partitionTypeDef','%f1737','%f1738','%f1739','%f1740','%f1741','subPartitions','%f1742','%f1743','%f1744','%f1745','partitionKeyAlgorithm','%f1746','%f1747','partitionDefinitions','%f1748','%f1749','%f1750','%f1751','%f1752','partitionDefinition','%f1753','%f1754','%f1755','%f1756','%f1757','%f1758','%f1759','%f1760','%f1761','partitionValuesIn','%f1762','%f1763','%f1764','%f1765','%f1766','%f1767','%f1768','%f1769','%f1770','partitionOption','%f1771','%f1772','%f1773','subpartitionDefinition','%f1774','partitionValueItemListParen','%f1775','%f1776','partitionValueItem','definerClause','ifExists','ifNotExists','%f1777','procedureParameter','%f1778','%f1779','functionParameter','collate','%f1780','typeWithOptCollate','schemaIdentifierPair','%f1781','viewRefList','%f1782','%f1783','%f1784','updateList','%f1785','%f1786','updateElement','%f1787','charsetClause','%f1788','fieldsClause','%f1789','fieldTerm','%f1790','linesClause','lineTerm','%f1791','%f1792','userList','%f1793','%f1794','%f1795','createUserList','%f1796','%f1797','%f1798','alterUserList','%f1799','%f1800','%f1801','createUserEntry','%f1802','%f1803','%f1804','%f1805','%f1806','%f1807','%f1808','%f1809','%f1810','%f1811','%f1812','%f1813','%f1814','%f1815','%f1816','alterUserEntry','%f1817','%f1818','%f1819','%f1820','%f1821','%f1822','%f1823','%f1824','%f1825','%f1826','%f1827','%f1828','%f1829','%f1830','%f1831','%f1832','%f1833','retainCurrentPassword','discardOldPassword','replacePassword','%f1834','userIdentifierOrText','%f1835','%f1836','%f1837','%f1838','user','likeClause','likeOrWhere','onlineOption','noWriteToBinLog','usePartition','%f1839','%f1840','fieldIdentifier','columnName','%f1841','%f1842','columnInternalRef','%f1843','columnInternalRefList','%f1844','%f1845','columnRef','insertIdentifier','indexName','indexRef','%f1846','tableWild','%f1847','%f1848','schemaName','schemaRef','procedureName','procedureRef','functionName','functionRef','triggerName','triggerRef','viewName','viewRef','tablespaceName','tablespaceRef','logfileGroupName','logfileGroupRef','eventName','eventRef','udfName','serverName','serverRef','engineRef','tableName','filterTableRef','%f1849','tableRefWithWildcard','%f1850','%f1851','%f1852','%f1853','%f1854','tableRef','%f1855','tableRefList','%f1856','%f1857','%f1858','tableAliasRefList','%f1859','%f1860','parameterName','labelIdentifier','labelRef','roleIdentifier','roleRef','pluginRef','componentRef','resourceGroupRef','windowName','pureIdentifier','%f1861','%f1862','identifier','%f1863','identifierList','%f1864','%f1865','identifierListWithParentheses','%f1866','qualifiedIdentifier','%f1867','simpleIdentifier','%f1868','%f1869','%f1870','%f1871','dotIdentifier','ulong_number','real_ulong_number','ulonglong_number','real_ulonglong_number','%f1872','%f1873','literal','%f1874','signedLiteral','%f1875','stringList','%f1876','%f1877','textStringLiteral','%f1878','textString','textStringHash','%f1879','%f1880','textLiteral','%f1881','%f1882','textStringNoLinebreak','%f1883','textStringLiteralList','%f1884','%f1885','numLiteral','boolLiteral','nullLiteral','temporalLiteral','floatOptions','standardFloatOptions','precision','textOrIdentifier','lValueIdentifier','roleIdentifierOrText','sizeNumber','parentheses','equal','optionType','varIdentType','setVarIdentType','identifierKeyword','%f1886','%f1887','%f1888','%f1889','%f1890','identifierKeywordsAmbiguous1RolesAndLabels','identifierKeywordsAmbiguous2Labels','labelKeyword','%f1891','%f1892','%f1893','identifierKeywordsAmbiguous3Roles','identifierKeywordsUnambiguous','%f1894','%f1895','%f1896','roleKeyword','%f1897','%f1898','%f1899','lValueKeyword','identifierKeywordsAmbiguous4SystemVariables','roleOrIdentifierKeyword','%f1900','%f1901','%f1902','roleOrLabelKeyword','%f1903','%f1904','%f1905','%f1906','%f1907','%f1908','%f1909'],'grammar'=>[0=>[[-1],[2001,2003]],1=>[[2004],[2873]],2=>[[-1],[0]],3=>[[755,2002],[-1]],4=>[[2009],[2221],[2414],[2470],[2476],[2005],[2479],[2485],[2504],[2508],[2524],[2571],[2598],[2603],[2856],[2860],[2934],[3079],[2006],[3103],[3278],[3301],[3314],[3361],[2007],[3443],[3534],[2008],[3945],[3954]],5=>[[2477]],6=>[[3090]],7=>[[3498]],8=>[[3925]],9=>[[11,2013]],10=>[[2191]],12=>[[2285],[0]],13=>[[2071],[2031],[422,4388,2012],[206,4390,2012],[2212],[2042],[2175],[2010],[2060],[2067],[2014]],14=>[[2029]],15=>[[33]],16=>[[844],[2015]],19=>[[2018],[0]],18=>[[373,480,383,165]],20=>[[451,845,2019]],23=>[[2022],[0]],22=>[[373,480,383,165]],24=>[[451,845,200,57,4435,2023]],25=>[[156],[140]],26=>[[2025,844,846]],27=>[[451,847]],28=>[[482,2016,316,265],[2020],[2024],[2026],[2027]],29=>[[244,2028]],30=>[[4386],[0]],31=>[[109,2030,2034]],32=>[[615,112,139,357]],33=>[[2229,2033],[2229]],34=>[[2033],[2032]],391=>[[4273],[0]],43=>[[2044],[0]],46=>[[2047],[0]],48=>[[2049],[0]],53=>[[2054],[0]],55=>[[2056],[0]],57=>[[2058],[0]],42=>[[2391,170,4400,2043,2046,2048,2053,2055,2057]],44=>[[383,490,3972]],2023=>[[371],[0]],47=>[[383,79,4023,418]],49=>[[453,590,4435]],52=>[[2051],[0]],51=>[[383,514]],54=>[[156],[140,2052]],56=>[[75,4469]],58=>[[147,3869]],59=>[[2062],[0]],60=>[[288,217,4398,4,603,4469,2059]],64=>[[2065,2064],[2065],[0]],62=>[[2066,2064]],2157=>[[750],[0]],65=>[[4157,2066]],66=>[[2345],[2359],[2362]],67=>[[503,4403,2318]],427=>[[4363],[0]],73=>[[2074],[0]],70=>[[2077],[0]],71=>[[2427,2073,574,4414,2070]],72=>[[232]],74=>[[2072]],78=>[[2079],[0]],80=>[[2081],[0]],77=>[[2078,2090],[2083,2080],[4216],[2173]],79=>[[2087,750]],81=>[[4216],[2173]],84=>[[2085],[0]],83=>[[2087,2084],[2112]],85=>[[750,2112]],88=>[[2089,2088],[2089],[0]],87=>[[2117,2088]],89=>[[750,2117]],90=>[[141,572],[234,572],[2105],[2092]],91=>[[722],[723]],92=>[[2091]],1441=>[[4364],[0]],1273=>[[3296,3273],[3296],[0]],1277=>[[3298,3277],[3298],[0]],107=>[[2108],[0]],104=>[[2170],[0]],105=>[[4,405,3441,2106],[148,405,4437],[438,405,3441,2174],[388,405,3441,2174,3441],[14,405,3441,2174],[62,405,2174,3273],[455,405,3441,2174,3277],[67,405,3441,4451],[597,405,2174],[454,405,3441,2107],[172,405,4435,645,574,4414,2104],[2109],[2110]],106=>[[4237],[404,4451]],108=>[[4437,248,4237]],109=>[[141,405,2174,572]],110=>[[234,405,2174,572]],115=>[[2116,2115],[2116],[0]],112=>[[2113,2115]],113=>[[2126],[4161]],114=>[[2126],[2117],[4161]],116=>[[750,2114]],117=>[[2162],[2165],[2170]],136=>[[72],[0]],128=>[[2153],[0]],147=>[[2148],[0]],2282=>[[4281],[0]],126=>[[4,2136,2129],[4,3993],[55,2136,4372,4435,4007,2128],[348,2136,4372,4007,2128],[148,2138],[140,263],[156,263],[11,2136,4372,2142],[2143],[2144],[2145],[2146],[453,2147,4405],[2149],[94,590,3847,2151,4282],[198],[393,45,2157],[2152]],1977=>[[3979],[0]],129=>[[4435,4007,3977,2128],[753,2242,748]],130=>[[4372]],131=>[[4372],[0]],132=>[[2131]],133=>[[2130],[2132]],134=>[[62,4435]],135=>[[86,4435]],137=>[[2154],[0]],138=>[[2136,4372,2137],[199,265,2133],[420,265],[2840,4380],[2134],[2135]],139=>[[3854]],140=>[[2139],[4458]],141=>[[506,4082]],142=>[[506,128,2140],[148,128],[2141]],143=>[[11,236,4380,4082]],144=>[[11,62,4435,3983]],145=>[[11,86,4435,3983]],146=>[[453,72,4372,590,4435]],148=>[[590],[17]],149=>[[453,2840,4380,590,4379]],150=>[[128]],151=>[[2150],[4150]],152=>[[615,403]],153=>[[6,4435],[191]],154=>[[471],[49]],2071=>[[2724],[0]],159=>[[2160,2159],[2160],[0]],157=>[[4442,4071,2159]],160=>[[750,4442,4071]],2262=>[[763],[0]],162=>[[9,4262,2163]],163=>[[128],[4435]],165=>[[287,4262,2166]],166=>[[128],[4435]],167=>[[2165],[0]],168=>[[2162],[0]],169=>[[2162,2167],[2165,2168]],170=>[[2172]],171=>[[645],[646]],172=>[[2171,625]],173=>[[452,403]],174=>[[10],[4437]],175=>[[572,4396,2189]],176=>[[4],[148]],180=>[[2179,2180],[2179],[0]],179=>[[4157,2208]],184=>[[2182],[0]],182=>[[2208,2180]],183=>[[434],[436]],185=>[[55,111,4469,2184],[2183],[371,1]],186=>[[2185]],187=>[[2200]],188=>[[2200],[0]],189=>[[2176,111,4469,2188],[2186],[453,590,4435],[2187]],467=>[[2194],[0]],191=>[[605,572,4396,506,2192,2467]],192=>[[724],[725]],196=>[[2197,2196],[2197],[0]],194=>[[2198,2196]],197=>[[4157,2198]],198=>[[2359]],202=>[[2203,2202],[2203],[0]],200=>[[2205,2202]],203=>[[4157,2205]],205=>[[238,4262,4487],[2350],[2352],[2359],[2206],[2362],[2369]],206=>[[2361]],208=>[[238,4262,4487],[2350],[2352]],374=>[[2376],[0]],372=>[[2378],[0]],212=>[[2374,2391,2372,636,4394,2214]],1233=>[[4374],[0]],214=>[[3233,17,2216]],215=>[[2218],[0]],216=>[[2251,2215]],219=>[[2220],[0]],218=>[[645,2219,62,391]],220=>[[50],[284]],221=>[[97,2225]],222=>[[2408]],223=>[[2412]],224=>[[2328]],225=>[[2228],[2233],[2270],[2258],[2280],[2308],[2373],[2382],[2290],[2316],[2324],[2396],[2222],[2223],[2224]],1391=>[[4275],[0]],227=>[[2229,2227],[2229],[0]],228=>[[109,3391,4385,2227]],229=>[[4212],[4206],[2230]],230=>[[4209]],441=>[[577],[0]],233=>[[2441,574,3391,4405,2240]],236=>[[2235],[0]],235=>[[753,2242,748]],237=>[[4156],[0]],238=>[[4216],[0]],239=>[[2248],[0]],240=>[[275,4414],[753,275,4414,748],[2236,2237,2238,2239]],243=>[[2244,2243],[2244],[0]],242=>[[2245,2243]],244=>[[750,2245]],245=>[[3978],[3993]],249=>[[2250],[0]],762=>[[17],[0]],248=>[[2249,2762,2251]],250=>[[458],[232]],251=>[[2608],[2636]],252=>[[755],[0]],253=>[[97,2254,2252,-1]],254=>[[2258],[2270],[2280]],265=>[[2266],[0]],269=>[[2283,2269],[2283],[0]],258=>[[2391,422,2261,4387,753,2265,748,2269,3869]],260=>[[3391]],261=>[[2260]],264=>[[2263,2264],[2263],[0]],263=>[[750,4277]],266=>[[4277,2264]],277=>[[2278],[0]],270=>[[2391,206,2273,4389,753,2277,748,474,4283,2269,3869]],272=>[[3391]],273=>[[2272]],276=>[[2275,2276],[2275],[0]],275=>[[750,4280]],278=>[[4280,2276]],279=>[[8],[0]],280=>[[2279,206,4401,474,2281,520,4469]],281=>[[556],[249],[437],[126]],283=>[[2286],[4023,137]],284=>[[2283,2284],[2283]],285=>[[2284]],286=>[[75,4469],[267,537],[373,537],[90,537],[433,537,112],[347,537,112],[537,496,2287]],287=>[[130],[250]],428=>[[2169],[0]],290=>[[2427,2299,2428]],291=>[[4083],[0]],292=>[[4379,2291]],2000=>[[2302],[0]],294=>[[2292],[4000]],295=>[[609],[0]],2001=>[[4078,4001],[4078],[0]],1988=>[[4085,3988],[4085],[0]],1991=>[[4086,3991],[4086],[0]],299=>[[2295,236,2294,2306,4001],[205,236,4379,2306,3988],[523,236,4379,2306,3991]],2002=>[[4379],[0]],304=>[[2305],[0]],302=>[[4002,2304]],303=>[[621],[599]],305=>[[2303,4076]],306=>[[383,4414,4073]],307=>[[2311],[0]],308=>[[288,217,4397,4,2309,4469,2307]],309=>[[603],[440]],313=>[[2314,2313],[2314],[0]],311=>[[2315,2313]],314=>[[4157,2315]],315=>[[2345],[2347],[2356],[2359],[2362],[2365]],316=>[[503,4402,199,112,648,4484,2318]],319=>[[2320,2319],[2320],[0]],318=>[[390,753,2321,2319,748]],320=>[[750,2321]],321=>[[224,4469],[109,4469],[618,4469],[406,4469],[519,4469],[398,4469],[413,4450]],325=>[[2326],[0]],323=>[[2336],[0]],324=>[[572,4395,2329,2325,2323]],326=>[[620,288,217,4398]],328=>[[605,572,4395,4,2334,2467]],329=>[[2333],[4,2334]],332=>[[2331],[0]],331=>[[4,2334]],333=>[[2332]],334=>[[111,4469]],338=>[[2339,2338],[2339],[0]],336=>[[2340,2338]],339=>[[4157,2340]],340=>[[2345],[2350],[2352],[2354],[2356],[2359],[2341],[2362],[2365],[2342],[2343]],341=>[[2361]],342=>[[2367]],343=>[[2369]],345=>[[238,4262,4487]],347=>[[2348,4262,4487]],348=>[[604],[441]],350=>[[23,4262,4487]],352=>[[324,4262,4487]],354=>[[181,4262,4487]],356=>[[368,4262,4451]],2257=>[[553],[0]],359=>[[4257,163,4262,4404]],361=>[[848,4262,4463]],362=>[[2363]],363=>[[638],[374]],365=>[[75,4262,4469]],367=>[[189,4262,4487]],369=>[[158,4262,4463]],370=>[[2375],[0]],373=>[[2370,2391,2372,636,4393,2214]],375=>[[394,458,2374],[2376]],376=>[[9,763,2377]],377=>[[602],[335],[578]],378=>[[537,496,2379]],379=>[[130],[250]],381=>[[2388],[0]],382=>[[2391,594,2385,4391,2386,2387,383,4414,200,153,487,2381,3869]],384=>[[3391]],385=>[[2384]],386=>[[28],[6]],387=>[[242],[614],[133]],388=>[[2390]],389=>[[197],[415]],390=>[[2389,4484]],398=>[[2399],[0]],403=>[[2404],[0]],405=>[[2406],[0]],396=>[[2391,170,3391,4399,383,490,3972,2398,2403,2405,147,3869]],399=>[[383,79,4023,418]],402=>[[2401],[0]],401=>[[383,514]],404=>[[156],[140,2402]],406=>[[75,4469]],408=>[[659,3391,3264]],411=>[[2413,2411],[2413],[0]],412=>[[394,458,523,718,710,4453,2411],[523,718,710,3391,4453,2411]],413=>[[357,580,4472],[715,580,4472],[717,4472,230,45,4453],[716,580,4472]],414=>[[148,2418]],415=>[[2464]],416=>[[2466]],417=>[[2468]],418=>[[2420],[2422],[2424],[2426],[2429],[2431],[2440],[2444],[2449],[2457],[2460],[2415],[2416],[2417]],939=>[[4274],[0]],420=>[[109,2939,4386]],422=>[[170,2939,4400]],424=>[[206,2939,4390]],426=>[[422,2939,4388]],429=>[[2427,236,4380,383,4414,2428]],436=>[[2437],[0]],431=>[[288,217,4398,2436]],435=>[[2434,2435],[2434],[0]],434=>[[4157,2438]],437=>[[2438,2435]],438=>[[2362],[2359]],440=>[[503,2939,4403]],446=>[[2447],[0]],444=>[[2441,2445,2939,4416,2446]],445=>[[574],[571]],447=>[[471],[49]],454=>[[2455],[0]],449=>[[572,4396,2454]],453=>[[2452,2453],[2452],[0]],452=>[[4157,2438]],455=>[[2438,2453]],457=>[[594,2939,4392]],461=>[[2462],[0]],460=>[[636,2939,4286,2461]],462=>[[471],[49]],464=>[[659,2939,3264]],466=>[[523,718,710,2939,4453]],468=>[[605,572,4396,2467]],472=>[[2473,2472],[2473],[0]],470=>[[453,2471,2474,2472]],471=>[[574],[571]],473=>[[750,2474]],474=>[[4414,590,4405]],475=>[[574],[0]],476=>[[597,2475,4414]],477=>[[234,574,203,4474]],481=>[[2482],[0]],479=>[[48,4388,2481]],1807=>[[3844],[0]],482=>[[753,3807,748]],487=>[[2488],[0]],484=>[[2503,2484],[2503],[0]],485=>[[2487,133,2484,2500]],486=>[[2714]],488=>[[2486]],489=>[[2829]],493=>[[2491],[0]],491=>[[2489]],1415=>[[2765],[0]],494=>[[2501],[0]],1646=>[[2723],[0]],855=>[[2654],[0]],498=>[[4420,621,2728,3415],[4414,2493,2494,3415,3646,2855]],500=>[[203,2498],[4420,203,2728,3415]],501=>[[2502]],502=>[[405,753,4437,748]],503=>[[431],[295],[431],[232]],504=>[[147,2507]],505=>[[2756]],506=>[[3844]],507=>[[2505],[2506]],508=>[[219,2513]],1421=>[[2653],[0]],511=>[[66],[435,2514,3415,3421]],901=>[[2829],[0]],513=>[[4414,387,2901],[4435,2511]],514=>[[2515],[4435,2518]],515=>[[191],[367]],516=>[[191],[367],[419],[268]],517=>[[763],[769],[765],[768],[764]],518=>[[2516],[2517,753,2555,748]],519=>[[2534],[0]],852=>[[232],[0]],596=>[[248],[0]],791=>[[4365],[0]],523=>[[2562],[0]],524=>[[242,2519,2852,2596,4414,2791,2533,2523]],525=>[[2561]],531=>[[2527],[0]],527=>[[2525]],528=>[[2561]],532=>[[2530],[0]],530=>[[2528]],533=>[[2536,2531],[506,4290,2532],[2547]],534=>[[295],[131],[223]],538=>[[2539],[0]],536=>[[2538,2544]],546=>[[2541],[0]],539=>[[753,2546,748]],542=>[[2543,2542],[2543],[0]],541=>[[4378,2542]],543=>[[750,4378]],544=>[[2545,2550]],545=>[[626],[627]],547=>[[2251],[753,2546,748,2251]],736=>[[2555],[0]],552=>[[2553,2552],[2553],[0]],550=>[[753,2736,748,2552]],553=>[[750,753,2736,748]],558=>[[2559,2558],[2559],[0]],555=>[[2556,2558]],556=>[[3559],[128]],557=>[[3559],[128]],559=>[[750,2557]],561=>[[17,4435,3233]],562=>[[383,151,265,614,4290]],572=>[[2573],[0]],903=>[[284],[0]],574=>[[2575],[0]],667=>[[4295],[0]],568=>[[2577],[0]],668=>[[4297],[0]],669=>[[4301],[0]],571=>[[281,2576,2572,2903,237,4469,2574,248,574,4414,2791,2667,2568,2668,2669,2581]],573=>[[295],[82]],575=>[[458],[232]],576=>[[112],[653]],577=>[[484,230,45,4465]],583=>[[2584],[0]],579=>[[2588],[0]],585=>[[2586],[0]],581=>[[2583,2579,2585]],582=>[[278],[484]],584=>[[232,787,2582]],586=>[[506,4290]],587=>[[2590],[0]],588=>[[753,2587,748]],593=>[[2594,2593],[2594],[0]],590=>[[2591,2593]],591=>[[4377],[3816]],592=>[[4377],[3816]],594=>[[750,2592]],599=>[[2600],[0]],598=>[[458,2599,2596,4414,2791,2601]],600=>[[295],[131]],601=>[[2536],[506,4290],[2547]],635=>[[2742],[0]],603=>[[2608,2635],[2605]],605=>[[753,2605,748],[2608,2662,2635],[2608,2742,2662]],610=>[[2611],[0]],618=>[[2619],[0]],608=>[[2610,2616,2618]],609=>[[2714]],611=>[[2609]],616=>[[2621,3646,3421],[2636,3646,3421]],617=>[[2673]],619=>[[2617]],625=>[[2626,2625],[2626],[0]],621=>[[2628,2625]],622=>[[663]],623=>[[608],[2622]],631=>[[2827],[0]],626=>[[2623,2631,2628]],633=>[[2634,2633],[2634],[0]],628=>[[2629,2633]],629=>[[2637],[2636]],630=>[[2637],[2636]],632=>[[811,2631,2630]],634=>[[2632]],636=>[[753,2608,2635,748]],637=>[[2647],[2638],[2639]],638=>[[2732]],639=>[[2735]],640=>[[2738,2640],[2738],[0]],641=>[[2662],[0]],642=>[[2725],[0]],644=>[[2720],[0]],645=>[[2679],[0]],649=>[[2650],[0]],647=>[[497,2640,2756,2641,2642,3415,2644,2645,2649]],648=>[[2681]],650=>[[2648]],651=>[[2636]],652=>[[10],[143],[555],[223],[536],[531],[532],[534]],653=>[[276,2656]],654=>[[276,2660]],658=>[[2659],[0]],656=>[[2660,2658]],657=>[[750],[381]],659=>[[2657,2660]],660=>[[4435],[2661]],661=>[[754],[791],[788],[787]],662=>[[248,2671]],663=>[[4484],[3816]],664=>[[4484],[3816]],670=>[[2666,2670],[2666],[0]],666=>[[750,2664]],671=>[[396,4463,2667,2668,2669],[150,4463],[2663,2670]],677=>[[2678],[0]],673=>[[422,13,753,2677,748]],676=>[[2675],[0]],675=>[[750,787]],678=>[[787,2676]],679=>[[221,3559]],682=>[[2683,2682],[2683],[0]],681=>[[699,2684,2682]],683=>[[750,2684]],684=>[[4431,17,2685]],685=>[[753,2696,748]],695=>[[2704],[0]],697=>[[2698],[0]],699=>[[2700],[0]],692=>[[4431],[0]],701=>[[2702],[0]],696=>[[405,45,3857,3646,2695],[2697,2723,2695],[2699,3646,2704],[2692,2701,3646,2695]],698=>[[405,45,3857]],700=>[[405,45,3857]],702=>[[405,45,3857]],703=>[[2710],[0]],704=>[[2705,2706,2703]],705=>[[484],[432],[683]],706=>[[2707],[2708]],707=>[[698,693],[4452,693],[754,693],[247,3559,3850,693],[101,487]],708=>[[30,2709,15,2709]],709=>[[2707],[698,682],[4452,682],[754,682],[247,3559,3850,682]],710=>[[680,2711]],711=>[[101,487],[217],[697],[373,690]],712=>[[665],[0]],715=>[[2716,2715],[2716],[0]],714=>[[645,2712,2718,2715]],716=>[[750,2718]],718=>[[4435,3233,17,2651]],719=>[[2721],[0]],720=>[[217,45,3857,2719]],721=>[[645,481],[2722]],722=>[[645,99]],723=>[[393,45,3857]],724=>[[18],[134]],725=>[[203,2726]],726=>[[149],[2728]],729=>[[2730,2729],[2730],[0]],728=>[[2767,2729]],730=>[[750,2767]],733=>[[2734,2733],[2734],[0]],732=>[[626,2737,2733]],734=>[[750,2737]],735=>[[574,4414]],737=>[[487,753,2736,748]],738=>[[2652],[535],[2739],[2740]],739=>[[533]],740=>[[325,763,4451]],741=>[[2745,2741],[2745]],742=>[[2741]],747=>[[2748],[0]],750=>[[2751],[0]],745=>[[200,2752,2747,2750],[287,251,508,346]],746=>[[668,4420]],748=>[[2746]],749=>[[2754]],751=>[[2749]],752=>[[614],[2753]],753=>[[508]],754=>[[669,670],[671]],758=>[[2759,2758],[2759],[0]],756=>[[2757,2758]],757=>[[2761],[775]],759=>[[750,2761]],1813=>[[2763],[0]],761=>[[4382],[3559,3813]],763=>[[2762,2764]],764=>[[4435],[4463]],765=>[[643,3559]],771=>[[2774,2771],[2774],[0]],767=>[[2770,2771]],768=>[[4435]],769=>[[2768],[732]],770=>[[2789],[752,2769,2772,747]],772=>[[2789,2771]],775=>[[2776],[0]],774=>[[2783,2767,2775],[2787,2767,2777],[2780,2789]],776=>[[383,3559],[621,4440]],777=>[[383,3559],[621,4440]],778=>[[239],[0]],786=>[[395],[0]],780=>[[359,2778,261],[359,2781,2786,261]],781=>[[272],[478]],784=>[[2785],[0]],783=>[[2784,261],[555]],785=>[[239],[98]],787=>[[2788,2786,261]],788=>[[272],[478]],789=>[[2794],[2795],[2799],[2806],[2790]],790=>[[2809]],793=>[[2834],[0]],794=>[[4414,2791,2901,2793]],795=>[[753,2796,748]],796=>[[2794],[2795]],801=>[[2802],[0]],799=>[[2651,2901,2801],[2805]],800=>[[4374]],802=>[[2800]],805=>[[726,2651,2901,3233]],806=>[[753,2807,748]],807=>[[2728],[2806]],809=>[[701,753,3559,750,4463,2811,748,2901]],812=>[[2813,2812],[2813],[0]],811=>[[71,753,2817,2812,748]],813=>[[750,2817]],819=>[[2820],[0]],1605=>[[174],[0]],1749=>[[2823],[0]],817=>[[4435,200,703],[4435,4117,2819,3605,704,4463,3749],[702,704,4463,2811]],818=>[[4281]],820=>[[2818]],821=>[[2825],[0]],822=>[[2824],[0]],823=>[[2824,2821],[2825,2822]],824=>[[2826,383,700]],825=>[[2826,383,165]],826=>[[165],[376],[128,4463]],827=>[[143],[10]],831=>[[2832],[0]],829=>[[2831,4435]],830=>[[763]],832=>[[17],[2830]],833=>[[2838,2833],[2838]],834=>[[2833]],836=>[[2843],[0]],837=>[[2846],[0]],838=>[[2839,2840,2836,753,2846,748],[620,2840,2836,753,2837,748]],839=>[[198],[232]],840=>[[265],[236]],1995=>[[2840],[0]],842=>[[420,265],[609,3995]],843=>[[200,2844]],844=>[[261],[393,45],[217,45]],847=>[[2848,2847],[2848],[0]],846=>[[2849,2847]],848=>[[750,2849]],849=>[[4435],[420]],858=>[[2859],[0]],904=>[[295],[0]],856=>[[2858,614,2904,2852,2728,506,4290,3415,3646,2855]],857=>[[2714]],859=>[[2857]],860=>[[2865],[2882],[2894],[2906]],861=>[[2875],[0]],881=>[[647],[0]],867=>[[2868],[0]],870=>[[2871],[0]],865=>[[543,592,2861],[77,2881,2867,2870]],1101=>[[373],[0]],868=>[[15,3101,54]],871=>[[3101,450]],873=>[[29,2881]],876=>[[2877,2876],[2877],[0]],875=>[[2878,2876]],877=>[[750,2878]],878=>[[645,85,517],[2880]],879=>[[649],[386]],880=>[[435,2879]],882=>[[489,4435],[480,2881,2892],[450,489,4435]],890=>[[2885],[0]],885=>[[15,3101,54]],891=>[[2888],[0]],888=>[[3101,450]],889=>[[489],[0]],892=>[[590,2889,4435],[2890,2891]],896=>[[2897,2896],[2897],[0]],894=>[[287,2895,2902,2896],[2898],[611,2900]],895=>[[571],[574]],897=>[[750,2902]],898=>[[287,244,200,27]],899=>[[244]],900=>[[571],[574],[2899]],902=>[[4414,2901,2905]],905=>[[435,2903],[2904,649]],906=>[[651,2920]],907=>[[543],[29]],917=>[[2909],[0]],909=>[[261],[472]],912=>[[2911],[0]],911=>[[200,340]],918=>[[2914],[0]],914=>[[566,2912]],919=>[[2916],[0]],916=>[[384,407]],920=>[[2907,2927,2917],[159,2927,2918],[417,2927],[77,2927,2919],[480,2927],[439,2921]],921=>[[2925],[0]],924=>[[2923],[0]],923=>[[94,652]],925=>[[2924]],931=>[[2932],[0]],927=>[[4465,2931]],930=>[[2929],[0]],929=>[[750,4450]],932=>[[750,4465,2930]],937=>[[2938,2937],[2938],[0]],934=>[[428,2935,289,2936],[2959],[468,2949,2937],[2943],[3047],[2944],[2956],[2945]],935=>[[32],[316]],936=>[[590,4469],[28,3559]],938=>[[750,2949]],942=>[[2941],[0]],941=>[[2939,3820]],943=>[[468,658,2942]],944=>[[2979]],945=>[[3077]],946=>[[2951],[0]],1717=>[[10],[0]],1469=>[[3867],[0]],949=>[[316,2946],[2950],[514,3717,3469]],950=>[[430,47]],951=>[[2955]],952=>[[4451]],953=>[[4453]],954=>[[2952],[2953]],955=>[[590,2954]],956=>[[281,2957,203,316]],957=>[[112],[574,4414]],959=>[[55,316,590,2961,3469]],962=>[[2963,2962],[2963],[0]],961=>[[2964,2962]],963=>[[750,2964]],964=>[[300,763,4472],[729,763,4472],[297,763,4472],[318,763,4472],[303,763,4472],[304,763,4450],[298,763,4450],[305,763,4450],[299,763,4450],[314,763,4450],[308,763,4472],[307,763,4472],[317,763,4472],[309,763,4472],[738,763,2967],[310,763,4472],[313,763,4472],[315,763,4450],[311,763,4469],[312,763,4472],[712,763,4472],[713,763,4450],[319,763,4450],[233,763,2970],[735,763,4463],[736,763,4450],[296,763,4450],[737,763,2965],[739,763,4450],[742,763,2966],[2968]],965=>[[4355],[376]],966=>[[743],[383],[744]],967=>[[4472],[376]],968=>[[301,763,4472],[302,763,4452],[447,763,4472],[448,763,4450]],974=>[[2975],[0]],970=>[[753,2974,748]],973=>[[2972,2973],[2972],[0]],972=>[[750,4450]],975=>[[4450,2973]],980=>[[2981,2980],[2981],[0]],983=>[[2984],[0]],979=>[[55,459,522,590,2986,3469],[55,459,190,3024,2980,2983]],981=>[[750,3024]],982=>[[3867]],984=>[[2982]],987=>[[2988,2987],[2988],[0]],986=>[[2989,2987]],988=>[[750,2989]],989=>[[2990,763,4472],[2991,763,4472],[2992,763,4472],[2993,763,4472],[2994,763,4450],[2995,763,4472],[2996,763,4452],[2997,763,4450],[2998,763,4450],[2999,763,4450],[3000,763,4450],[817,763,4450],[3001,763,4450],[3002,763,4463],[3003,763,4450],[3004,763,4450],[3005,763,4472],[3006,763,4472],[3007,763,4472],[3008,763,4469],[3009,763,4472],[3010,763,4472],[3011,763,4472],[3012,763,4450],[3013,763,4472],[3014,763,2967],[3015,763,4472],[3016,763,4450],[729,763,4472],[233,763,2970],[841,763,4450],[737,763,2965],[739,763,4450],[742,763,2966],[842,763,4450],[447,763,4472],[448,763,4450]],990=>[[814],[297]],991=>[[820],[300]],992=>[[838],[318]],993=>[[823],[303]],994=>[[824],[304]],995=>[[821],[301]],996=>[[822],[302]],997=>[[813],[296]],998=>[[819],[319]],999=>[[816],[298]],1000=>[[826],[305]],1001=>[[818],[299]],1002=>[[815],[735]],1003=>[[839],[736]],1004=>[[827],[314]],1005=>[[828],[308]],1006=>[[829],[307]],1007=>[[830],[309]],1008=>[[832],[311]],1009=>[[833],[312]],1010=>[[834],[313]],1011=>[[831],[310]],1012=>[[835],[315]],1013=>[[837],[317]],1014=>[[836],[738]],1015=>[[825],[712]],1016=>[[840],[713]],1018=>[[3026],[0]],1020=>[[3030],[0]],1022=>[[3034],[0]],1023=>[[3039],[0]],1024=>[[460,763,753,3018,748],[461,763,753,3018,748],[462,763,753,3020,748],[463,763,753,3020,748],[464,763,753,3022,748],[465,763,753,3022,748],[466,763,753,3023,748]],1027=>[[3028,3027],[3028],[0]],1026=>[[4386,3027]],1028=>[[750,4386]],1031=>[[3032,3031],[3032],[0]],1030=>[[4406,3031]],1032=>[[750,4406]],1035=>[[3036,3035],[3036],[0]],1034=>[[3037,3035]],1036=>[[750,3037]],1037=>[[4472]],1040=>[[3041,3040],[3041],[0]],1039=>[[4284,3040]],1041=>[[750,4284]],1045=>[[3073],[0]],1048=>[[3049],[0]],1047=>[[543,514,3045,3048,3058,3469],[552,514,3045,3469]],1049=>[[613,3051]],1056=>[[3057,3056],[3057],[0]],1051=>[[3055,3056]],1052=>[[530],[528]],1053=>[[3052,763,4465]],1054=>[[529]],1055=>[[2968],[3053],[3054]],1057=>[[750,2968]],1058=>[[3071],[0]],1067=>[[3060],[0]],1060=>[[618,763,4465]],1068=>[[3062],[0]],1062=>[[406,763,4465]],1069=>[[3064],[0]],1064=>[[129,763,4465]],1070=>[[3066],[0]],1066=>[[409,763,4465]],1071=>[[3067,3068,3069,3070]],1074=>[[3075,3074],[3075],[0]],1073=>[[3076,3074]],1075=>[[750,3076]],1076=>[[449],[538]],1077=>[[3078,210]],1078=>[[543],[552]],1079=>[[417,4435,203,3080],[3083],[3081,417,4435]],1080=>[[4469],[3816]],1081=>[[123],[148]],1084=>[[3085],[0]],1083=>[[173,4435,3084]],1085=>[[621,3087]],1088=>[[3089,3088],[3089],[0]],1087=>[[3816,3088]],1089=>[[750,3816]],1090=>[[677,3097]],1096=>[[3092],[0]],1092=>[[200,459]],1093=>[[3100],[0]],1094=>[[244,203,4360,749,4450,230,45,4463,3093]],2183=>[[4489],[0]],1097=>[[284,112,139,4183,4463],[676,3096],[3094]],1099=>[[3102],[0]],1100=>[[3102],[112,139,4183,4463,3099]],1102=>[[467,3101,539]],1103=>[[3104],[3119],[3158],[3162],[3193],[3198],[3105]],1104=>[[3107]],1105=>[[3258]],1109=>[[3110],[0]],1107=>[[11,618,3109,3111]],1108=>[[4274]],1110=>[[3108]],1111=>[[3114],[3117,3122]],1112=>[[3118],[4360]],1113=>[[10],[369],[3264]],1114=>[[3112,128,659,3113]],1115=>[[4313]],1116=>[[4309]],1117=>[[3115],[3116]],1118=>[[618,4488]],1119=>[[97,618,3121,4309,3132,3122]],1120=>[[4275]],1121=>[[3120],[0]],1122=>[[3131],[0]],1123=>[[75],[812]],1124=>[[3123,4465]],1130=>[[3126],[0]],1126=>[[3124]],1127=>[[3137],[0]],1128=>[[3141],[0]],1129=>[[3144,3129],[3144],[0]],1131=>[[3127,3128,3129,3130]],1132=>[[3136],[0]],1135=>[[3134],[0]],1134=>[[128,659,3264]],1136=>[[3135]],1137=>[[467,3139]],1138=>[[539],[650],[369]],1139=>[[3251],[3138]],1142=>[[3143,3142],[3143]],1141=>[[645,3142]],1143=>[[322,4450],[327,4450],[321,4450],[328,4450]],1144=>[[2,3145],[406,3155],[740,3156],[741,4451]],1145=>[[287],[611]],1154=>[[3147],[0]],1147=>[[247,4451,122],[365],[128]],1148=>[[4451],[128]],1149=>[[4451,122],[128]],1152=>[[3151],[0]],1151=>[[128],[719]],1153=>[[467,101,3152]],1155=>[[177,3154],[705,3148],[706,247,3149],[3153]],1156=>[[4451],[698]],1160=>[[3161],[0]],1158=>[[148,618,3160,4305]],1159=>[[4274]],1161=>[[3159]],1162=>[[215,3176]],1165=>[[3164],[0]],1164=>[[645,660,391]],1166=>[[3225,590,4305,3165]],1210=>[[421],[0]],1168=>[[3225],[10,3210]],1175=>[[3170],[0]],1170=>[[645,215,391]],1218=>[[3223],[0]],1172=>[[3190],[0]],1173=>[[3180],[0]],1174=>[[3189],[0]],1176=>[[3166],[3168,383,3218,3245,590,3177,3172,3173,3174],[427,383,4360,590,3177,3175]],1177=>[[3178],[3179]],1178=>[[4309]],1179=>[[4305]],1180=>[[3182],[3183]],1181=>[[3256,3181],[3256]],1182=>[[645,3181]],1183=>[[645,215,391]],1184=>[[663,3264]],1185=>[[645,659,3187]],1186=>[[3184],[0]],1187=>[[3264],[10,3186],[369],[128]],1188=>[[3185],[0]],1189=>[[17,618,3188]],1190=>[[3191]],1191=>[[3137]],1194=>[[3195,3194],[3195],[0]],1193=>[[453,618,4360,590,4360,3194]],1195=>[[750,4360,590,4360]],1200=>[[3201],[0]],1213=>[[3214],[0]],1198=>[[477,3200,3211,3213]],1199=>[[4274]],1201=>[[3199]],1202=>[[3225,203,4305]],1203=>[[3215],[0]],1207=>[[3205],[0]],1205=>[[3203,203,4305]],1208=>[[383,3218,3245,3207]],1209=>[[3208],[750,215,391,203,4305]],1211=>[[3202],[3225,3215,203,4305],[10,3210,3209],[427,383,4360,203,4305]],1212=>[[232,610,618]],1214=>[[3212]],1215=>[[3217],[3222]],1217=>[[383,3218,3245]],1221=>[[3220],[0]],1220=>[[383,3218,3245]],1222=>[[3221]],1223=>[[574],[206],[422]],1226=>[[3227,3226],[3227],[0]],1225=>[[3231,3226]],1227=>[[750,3231]],1241=>[[3242],[0]],1230=>[[483],[0]],1231=>[[3235],[3236,3233],[3238],[3239],[215,391],[509,110],[97,3241],[287,571],[459,3243],[509,636],[11,3230]],1232=>[[792],[746,4484]],1234=>[[4486,3232],[4486,3233]],1235=>[[3234]],1236=>[[497],[242],[614],[443]],1237=>[[97],[148]],1238=>[[3237,659]],1239=>[[133],[616],[236],[148],[173],[451],[510],[423],[188],[427],[565],[170],[594]],1240=>[[483],[572],[618],[636]],1242=>[[577,571],[3240]],1243=>[[65],[514]],1246=>[[3247],[0]],1245=>[[775,3246],[4386,751,3249],[4386],[4414]],1247=>[[751,775]],1248=>[[4414]],1249=>[[775],[3248]],1253=>[[3254,3253],[3254],[0]],1251=>[[3255,3253]],1252=>[[15],[0]],1254=>[[3252,3255]],1255=>[[63,4465],[259,4465],[559,4465]],1256=>[[215,391],[322,4450],[327,4450],[321,4450],[328,4450]],1261=>[[3262],[0]],1258=>[[506,659,3264],[506,659,3259],[506,128,659,3260,590,3264],[506,659,10,3261]],1259=>[[369],[128]],1260=>[[3264],[369],[10]],1262=>[[663,3264]],1265=>[[3266,3265],[3266],[0]],1264=>[[3268,3265]],1266=>[[750,3268]],1269=>[[3270],[0]],1268=>[[4486,3269]],1270=>[[746,4484],[792]],1281=>[[3282],[0]],1285=>[[3286],[0]],1278=>[[14,3441,3279,4416,3281],[62,3283,4416,3273],[61,3284,4416,3285],[388,3441,3287,4416],[455,3441,3288,4416,3277]],1279=>[[574],[571]],1280=>[[3291]],1282=>[[3280]],1283=>[[574],[571]],1284=>[[574],[571]],1286=>[[431],[180]],1287=>[[574],[571]],1288=>[[574],[571]],1292=>[[3293],[0]],1294=>[[3295],[0]],1291=>[[614,674,383,4437,3292,3294],[148,674,383,4437]],1293=>[[645,787,675]],1295=>[[621,112,4463]],1296=>[[200,615],[3297]],1297=>[[431],[184],[333],[180],[56]],1298=>[[431],[180],[619]],1302=>[[3303],[0]],1304=>[[3305,3304],[3305],[0]],1301=>[[245,410,4435,520,4463],[245,664,4474,3302],[607,410,4428],[607,664,4429,3304]],1303=>[[506,3311]],1305=>[[750,4429]],1306=>[[214],[658]],1307=>[[3306],[0]],1308=>[[3307,3820,4489,3309]],1309=>[[383],[3559]],1312=>[[3313,3312],[3313],[0]],1311=>[[3308,3312]],1313=>[[750,3308]],1314=>[[506,3316]],1317=>[[3318],[0]],1316=>[[592,3334],[406,3317,4489,3325],[3331],[3348,3345],[4490,3355]],1318=>[[200,4360]],1319=>[[382,753,4465,748]],1320=>[[406,753,4465,748]],2343=>[[4353],[0]],2344=>[[4351],[0]],1325=>[[4465,4343,4344],[4465,4343,4344],[3319],[3320]],1328=>[[3327],[0]],1327=>[[200,4360]],1331=>[[406,3328,590,734,4343,4344]],1335=>[[3336],[0]],1337=>[[3338],[0]],1334=>[[3339,3335],[3341,3337]],1336=>[[750,3341]],1338=>[[750,3339]],1339=>[[435,3340]],1340=>[[649],[386]],1341=>[[258,274,3343]],1342=>[[76],[601]],1343=>[[456,435],[435,3342],[500]],1346=>[[3347,3346],[3347],[0]],1345=>[[3346]],1347=>[[750,3352]],1348=>[[3820,4489,3357],[4295],[3816,4489,3559],[3354,4489,3357],[356,3351]],1349=>[[128]],1351=>[[4489,3559],[4150,4282],[3349]],1352=>[[4490,3820,4489,3357],[3348]],1353=>[[4492],[0]],1354=>[[745,3353,3820]],1355=>[[3356,3345],[592,3334]],1356=>[[3820,4489,3357]],1357=>[[3559],[3358],[3360]],1358=>[[128],[383],[10],[32]],1359=>[[487],[710]],1360=>[[3359]],1361=>[[509,3430]],1362=>[[22]],1363=>[[4404],[10]],1364=>[[547],[354],[289]],1365=>[[203],[251]],1366=>[[32],[316]],1368=>[[225],[547,3434,3469]],1369=>[[33],[446]],1409=>[[3371],[0]],1371=>[[251,4465]],1410=>[[3373],[0]],1373=>[[203,4452]],1374=>[[180]],1413=>[[3376],[0]],1376=>[[3374]],1377=>[[236],[235],[263]],1378=>[[639],[166]],1381=>[[3380,3381],[3380],[0]],1380=>[[750,3439]],1419=>[[3383],[0]],1383=>[[3439,3381]],1420=>[[3385],[0]],1385=>[[200,430,787]],1386=>[[547],[631]],1387=>[[93]],1427=>[[3389],[0]],1389=>[[200,4360]],1390=>[[618,4360]],1392=>[[109,3391,4386],[170,4400],[206,4390],[422,4388],[574,4414],[594,4392],[636,4394],[3390]],1429=>[[4362],[0]],1406=>[[3431],[0]],1414=>[[3438],[0]],1432=>[[204],[0]],1422=>[[4490],[0]],1430=>[[3362],[110,3429],[3406,571,3414,3429],[3432,593,3414,3429],[169,3414,3429],[574,547,3414,3429],[387,571,3414,3429],[408],[163,3363,3364],[3406,71,3365,4414,3414,3429],[3366,289],[514,3368],[3369,169,3409,3410,3421,3469],[3413,3377,3437,4414,3414,3415],[4257,162],[95,753,775,748,3378],[639,3421],[166,3421],[426],[425,3419,3420,3421],[3422,3386,3429],[3432,424],[3847,3429],[70,3429],[3387],[421],[216,200,4360,621,4305],[216,3427],[316,547],[97,3392],[422,547,3429],[206,547,3429],[422,68,4388],[206,68,4390]],1431=>[[204],[3433]],1433=>[[180,3432]],1434=>[[3436],[0]],1435=>[[370],[0]],1436=>[[3435]],1437=>[[203],[251]],1438=>[[3437,4435]],1439=>[[40,255],[91,568],[400,185],[3440]],1440=>[[10],[96],[256],[334],[522],[567]],1449=>[[3450],[0]],1443=>[[33,4469],[47,236,3452,251,3444],[196,3441,3448],[266,3449,3559],[281,236,248,47,3485],[3451]],1444=>[[4435],[128]],1447=>[[3446,3447],[3446],[0]],1446=>[[750,3470]],1448=>[[3476],[3470,3447]],1450=>[[84],[430]],1451=>[[510]],1452=>[[3454],[3460]],1455=>[[3456,3455],[3456],[0]],1454=>[[3458,3455]],1456=>[[750,3458]],1492=>[[3462],[0]],1458=>[[4414,3492]],1460=>[[4414,405,753,2174,748,3492]],1461=>[[3465],[0]],1462=>[[2840,753,3461,748]],1463=>[[4435],[420]],1466=>[[3467,3466],[3467],[0]],1465=>[[3463,3466]],1467=>[[750,3463]],1468=>[[3474],[0]],1470=>[[3471],[3468,289],[445,289,3469],[3472],[3473]],1471=>[[136],[225],[421],[547],[617]],1472=>[[430,47]],1473=>[[389]],1474=>[[32],[163],[165],[208],[515]],1479=>[[3480],[0]],1476=>[[3477,3479]],1477=>[[571],[574]],1478=>[[3481],[0]],1480=>[[645,435,287],[4416,3478]],1481=>[[3482],[645,435,287]],1482=>[[200,179]],1486=>[[3487],[0]],1485=>[[4414,3497,3492,3486],[3489]],1487=>[[232,270]],1490=>[[3491,3490],[3491],[0]],1489=>[[3494,3490]],1491=>[[750,3494]],1495=>[[3496],[0]],1494=>[[4414,3492,3495]],1496=>[[232,270]],1497=>[[405,753,2174,748]],1498=>[[3503],[3522],[3524],[3533]],1518=>[[3507],[0]],1519=>[[3516],[0]],1520=>[[3517],[0]],1503=>[[97,709,217,4435,599,4183,3504,3518,3519,3520]],1504=>[[618],[710]],1509=>[[3510,3509],[3510],[0]],1507=>[[711,4183,3512,3509]],1510=>[[4157,3512]],1513=>[[3514],[0]],1512=>[[787,3513]],1514=>[[773,787]],1516=>[[708,4183,787]],1517=>[[156],[140]],1532=>[[198],[0]],1522=>[[11,709,217,4430,3518,3519,3520,3532]],1525=>[[3526],[0]],1524=>[[506,709,217,4435,3525]],1526=>[[200,3528]],1530=>[[3531,3530],[3531],[0]],1528=>[[4451,3530]],1531=>[[4157,4451]],1533=>[[148,709,217,4430,3532]],1534=>[[3542],[3537],[3555],[3556],[3535]],1535=>[[3557]],1539=>[[3540],[0]],1537=>[[3538,4414,3539]],1538=>[[178],[135],[134]],1540=>[[4465],[4377]],1549=>[[3550],[0]],1542=>[[3543,3549,3551]],1543=>[[178],[135],[134]],1544=>[[180]],1545=>[[404]],1546=>[[201,763,4484]],1547=>[[14,201,763,4484]],1548=>[[14]],1550=>[[3544],[3545],[3546],[3547],[3548]],1551=>[[2603],[3553],[3554]],1552=>[[2485],[2524],[2598],[2856]],1553=>[[3552]],1554=>[[200,84,4451]],1555=>[[222,4484]],1556=>[[620,4435]],1557=>[[714]],1558=>[[3567,3558],[3567],[0]],1559=>[[3561,3558]],1564=>[[3565],[0]],1561=>[[3571,3564],[3566]],1562=>[[596],[183],[610]],2040=>[[3848],[0]],1565=>[[257,4040,3562]],1566=>[[371,3559]],1567=>[[3568,3559],[654,3559],[3569,3559]],1568=>[[15],[770]],1569=>[[394],[772]],1570=>[[3573,3570],[3573],[0]],1571=>[[3577,3570]],1573=>[[257,4040,376],[3575,3574,2651],[3575,3577]],1574=>[[10],[16]],1575=>[[763],[777],[764],[765],[768],[769],[776]],1581=>[[3582],[0]],1577=>[[3589,3581]],1578=>[[668],[0]],1579=>[[733,3578,3855]],1582=>[[4040,3584],[3579],[521,275,3589]],1586=>[[3587],[0]],1584=>[[251,3585],[30,3589,15,3577],[275,3596,3586],[444,3589]],1585=>[[2651],[753,3844,748]],1587=>[[168,3596]],1588=>[[3590,3588],[3590],[0]],1589=>[[3596,3588]],1590=>[[760,3589],[3591,3589],[3592,3589],[3593,247,3559,3850],[3594,3589],[757,3589],[759,3589]],1591=>[[775],[762],[774],[145],[349]],1592=>[[778],[773]],1593=>[[778],[773]],1594=>[[779],[780]],1597=>[[3598,3597],[3598],[0]],1596=>[[3600,3597]],1598=>[[761,3600]],1601=>[[3602],[0]],1600=>[[3612,3601]],1602=>[[69,4484]],1613=>[[3614],[0]],1604=>[[487],[0]],1606=>[[3725],[0]],1607=>[[3624],[0]],1881=>[[3559],[0]],1622=>[[3623,3622],[3623]],1610=>[[3828],[0]],1611=>[[3626],[0]],1612=>[[4456],[3649],[3815,3613],[754],[3615],[3616],[3617,3596],[3849,3596],[3604,753,3844,748],[3605,2651],[752,4435,3559,747],[320,3719,7,753,3589,3606,748],[32,3596],[3621],[52,753,3559,17,3838,3607,748],[51,3881,3622,3610,159],[94,753,3559,750,3838,748],[94,753,3559,621,4150,748],[128,753,4444,748],[626,753,4444,748],[247,3559,3850,778,3559],[3808],[3741],[4377,3611]],1614=>[[4489,3559]],1615=>[[3692]],1616=>[[3698]],1617=>[[778],[773],[758]],1618=>[[247],[0]],2106=>[[4149],[0]],1620=>[[52,753,3559,21,586,843,3618,4463,17,113,4106,748]],1621=>[[3620]],1623=>[[3826,3827]],1624=>[[3625]],1625=>[[731]],1626=>[[3627],[3628]],1627=>[[766,4463]],1628=>[[767,4463]],1645=>[[143],[0]],1651=>[[3652],[0]],1655=>[[3656],[0]],1659=>[[3660],[0]],1664=>[[3665],[0]],1667=>[[3668],[0]],1670=>[[3671],[0]],1673=>[[3674],[0]],1676=>[[3677],[0]],1679=>[[3680],[0]],1682=>[[3683],[0]],1685=>[[3686],[0]],1687=>[[3688],[0]],1690=>[[3691],[0]],1649=>[[26,753,3645,3718,748,3651],[3653,753,3718,748,3655],[3657],[95,753,3717,775,748,3659],[95,753,3662,748,3664],[345,753,3645,3718,748,3667],[326,753,3645,3718,748,3670],[551,753,3718,748,3673],[632,753,3718,748,3676],[548,753,3718,748,3679],[635,753,3718,748,3682],[564,753,3645,3718,748,3685],[218,753,3645,3844,3646,3687,748,3690]],1650=>[[3705]],1652=>[[3650]],1653=>[[35],[36],[38]],1654=>[[3705]],1656=>[[3654]],1657=>[[3716]],1658=>[[3705]],1660=>[[3658]],1662=>[[3717,775],[3718],[143,3844]],1663=>[[3705]],1665=>[[3663]],1666=>[[3705]],1668=>[[3666]],1669=>[[3705]],1671=>[[3669]],1672=>[[3705]],1674=>[[3672]],1675=>[[3705]],1677=>[[3675]],1678=>[[3705]],1680=>[[3678]],1681=>[[3705]],1683=>[[3681]],1684=>[[3705]],1686=>[[3684]],1688=>[[499,4465]],1689=>[[3705]],1691=>[[3689]],1692=>[[672,753,3844,748]],1693=>[[3708],[0]],1697=>[[3712],[0]],1703=>[[3704],[0]],1698=>[[3699,4488,3705],[688,3855,3705],[3700,753,3559,3693,748,3697,3705],[3701,3854,3697,3705],[687,753,3559,750,3596,748,3703,3697,3705]],1699=>[[696],[694],[679],[678],[692]],1700=>[[686],[684]],1701=>[[681],[685]],1702=>[[191],[268]],1704=>[[203,3702]],1705=>[[691,3706]],1706=>[[4431],[2685]],1710=>[[3711],[0]],1708=>[[750,3709,3710]],1709=>[[4452],[754],[4435],[3816]],1711=>[[750,3559]],1712=>[[3713,689]],1713=>[[695],[232]],1715=>[[3705],[0]],1716=>[[667,753,3718,748,3715],[666,753,3718,750,3718,748,3715]],1718=>[[3717,3559]],1719=>[[3721],[753,3721,748]],1722=>[[3723,3722],[3723],[0]],1721=>[[4444,3722]],1723=>[[750,4444]],1726=>[[3727],[0]],1725=>[[251,41,346],[251,359,267,346,3726],[645,430,176]],1727=>[[645,430,176]],1742=>[[3743],[0]],2359=>[[4488],[0]],1744=>[[3745,3744],[3745]],1751=>[[3752],[0]],2030=>[[3775],[0]],1757=>[[3758],[0]],1761=>[[3762],[0]],1741=>[[60,753,3844,3742,748],[105,4359],[116,3854],[122,3854],[229,3854],[242,753,3559,750,3559,750,3559,750,3559,748],[247,753,3559,3744,748],[3750],[272,753,3559,750,3559,748],[343,3854],[350,3854],[478,753,3559,750,3559,748],[495,3854],[586,3854],[583,753,3559,3751,748],[3790],[618,4488],[626,3854],[656,3854],[3753,753,3559,750,3754,748],[100,4359],[108,4030],[3755,753,3559,750,247,3559,3850,748],[182,753,3850,203,3559,748],[213,753,3789,750,3559,748],[372,4030],[414,753,3589,251,3559,748],[3798],[569,4030],[3756,753,3852,750,3559,750,3559,748],[622,4359],[624,4030],[623,4030],[19,3854],[58,3854],[67,3853],[70,3854],[109,4488],[231,753,3559,750,3559,750,3559,748],[201,753,3559,750,3559,3757,748],[337,3854],[349,753,3559,750,3559,748],[3759],[3760],[429,3854],[457,753,3559,750,3559,748],[458,753,3559,750,3559,750,3559,748],[476,3854],[485,4488],[597,753,3559,750,3559,748],[640,753,3559,3761,748],[641,753,3559,17,60,748],[641,753,3559,3770,748],[3772]],1743=>[[621,4150]],1745=>[[750,3559]],1748=>[[3747],[0]],1747=>[[851,3838]],1750=>[[850,753,3596,750,4469,3748,3749,748]],1752=>[[750,3559]],1753=>[[5],[558]],1754=>[[3559],[247,3559,3850]],1755=>[[114],[115]],1756=>[[584],[585]],1758=>[[750,3559]],1759=>[[382,753,4469,748]],1760=>[[406,3854]],1762=>[[750,3559]],1768=>[[3764],[0]],1764=>[[17,60,4148]],1765=>[[3778]],1769=>[[3767],[0]],1767=>[[3765]],1770=>[[17,32,4148],[3768,3769],[750,4450,750,4450,750,4450]],1772=>[[3773],[211,753,3807,748],[279,3853],[351,3853],[352,3853],[353,3853],[411,753,3559,750,3559,748],[412,3853]],1773=>[[90,753,3559,750,3559,748]],1774=>[[3776],[0]],1775=>[[753,3774,748]],1776=>[[3777]],1777=>[[787]],1778=>[[274,3782]],1781=>[[3780,3781],[3780],[0]],1780=>[[750,3784]],1782=>[[4451,773,4451],[3784,3781]],1787=>[[3788],[0]],1784=>[[4451,3787]],1785=>[[18],[134]],1786=>[[476],[0]],1788=>[[3785,3786],[476]],1789=>[[116],[586],[113],[583]],1790=>[[595,753,3797,748]],1793=>[[3792],[0]],1792=>[[203,3559]],1797=>[[3559,3793],[269,3881,203,3559],[591,3881,203,3559],[43,3881,203,3559]],1798=>[[563,753,3559,3805,748]],1803=>[[3800],[0]],1800=>[[750,3559]],1804=>[[3802],[0]],1802=>[[200,3559]],1805=>[[750,3559,3803],[203,3559,3804]],1806=>[[3810],[0]],1808=>[[4432,753,3806,748],[4442,753,3807,748]],1811=>[[3812,3811],[3812],[0]],1810=>[[3814,3811]],1812=>[[750,3814]],1814=>[[3559,3813]],1815=>[[3816],[3819]],1816=>[[746,4484],[792]],1817=>[[4491],[0]],2445=>[[4449],[0]],1819=>[[745,3817,4484,4445]],1820=>[[3825],[128,4449]],1822=>[[4435,4445]],1824=>[[4485,4445]],1825=>[[3822],[3824]],1826=>[[642,3559]],1827=>[[582,3559]],1828=>[[154,3559]],2111=>[[4133],[0]],2116=>[[4141],[0]],1834=>[[249],[0]],2092=>[[4481],[0]],1838=>[[32,4111],[60,4111,4116],[4130,4111],[512,3834],[612,3834],[116],[586,4106],[113,4106],[126,4092],[656],[3839],[3840],[3842]],1839=>[[262]],1840=>[[4132]],1841=>[[4482],[0]],1842=>[[195,3841]],1845=>[[3846,3845],[3846],[0]],1844=>[[3559,3845]],1846=>[[750,3559]],1847=>[[60,506],[58]],1848=>[[371],[800]],1849=>[[771],[800]],1850=>[[3852],[3851]],1851=>[[494],[341],[342],[226],[228],[227],[119],[121],[120],[118],[655]],1852=>[[337],[495],[343],[229],[122],[640],[350],[429],[656]],1853=>[[753,3844,748]],1854=>[[753,3559,748]],1855=>[[753,3596,748]],1858=>[[3859,3858],[3859],[0]],1857=>[[3861,3858]],1859=>[[750,3861]],1861=>[[3559,4071]],1864=>[[3865,3864],[3865],[0]],1863=>[[3866,3864]],1865=>[[750,3866]],1866=>[[3559]],1867=>[[3868]],1868=>[[200,57,4472]],1869=>[[2004],[3870],[3871],[3884],[3889],[3890],[3896],[3897],[3923],[3922],[3963],[3966],[3964]],1870=>[[475,3559]],1871=>[[231,3873,159,231]],1874=>[[3875],[0]],1873=>[[3559,3876,3874]],1875=>[[155,3873],[154,3878]],1876=>[[582,3878]],1879=>[[3880,3879],[3880]],1878=>[[3879]],1880=>[[3869,755]],1885=>[[3886,3885],[3886]],1883=>[[3887],[0]],1884=>[[51,3881,3885,3883,159,51]],1886=>[[3826,3876]],1887=>[[154,3878]],1895=>[[4425],[0]],1889=>[[3891,3894,3895]],1890=>[[3894]],1891=>[[4424,749]],1892=>[[3902],[0]],1893=>[[3878],[0]],1894=>[[29,3892,3893,159]],1896=>[[3891,3897,3895]],1897=>[[3898],[3899],[3900]],1898=>[[294,3878,159,294]],1899=>[[644,3559,147,3878,159,644]],1900=>[[457,3878,613,3559,159,457]],1903=>[[3904,3903],[3904]],1902=>[[3903]],1904=>[[3905,755]],1905=>[[3908],[3911],[3916],[3921]],1909=>[[3910],[0]],1908=>[[127,4437,4117,4282,3909]],1910=>[[128,3559]],1911=>[[127,4435,83,200,3912]],1912=>[[4450],[3914]],1913=>[[627],[0]],1914=>[[526,3913,4469]],1918=>[[3919,3918],[3919],[0]],1916=>[[127,3917,219,200,3920,3918,3869]],1917=>[[92],[175],[605]],1919=>[[750,3920]],1920=>[[3912],[4435],[527],[3848,202],[525]],1921=>[[127,4435,106,200,2603]],1922=>[[260,4425]],1923=>[[271,4425]],1927=>[[3928],[0]],1925=>[[207,3927,138,3935]],1926=>[[540]],1928=>[[101],[3926]],1933=>[[3930,3933],[3930],[0]],1930=>[[750,3937]],1934=>[[3932,3934],[3932],[0]],1932=>[[750,3940]],1935=>[[3937,3933],[83,3936,3940,3934]],1936=>[[4456],[3815],[4442]],1937=>[[3938,763,3939]],1938=>[[3815],[4435]],1939=>[[377],[485]],1940=>[[3941,763,3942]],1941=>[[3815],[4435]],1942=>[[3943],[473]],1943=>[[64],[557],[87],[89],[88],[53],[492],[576],[73],[107],[336],[355]],1950=>[[3951],[0]],1945=>[[511,3946,3950]],1946=>[[4435],[3914]],1949=>[[3948,3949],[3948],[0]],1948=>[[750,3962]],1951=>[[506,3962,3949]],1955=>[[3956],[0]],1960=>[[3961],[0]],1954=>[[469,3955,3960]],1956=>[[4435],[3914]],1959=>[[3958,3959],[3958],[0]],1958=>[[750,3962]],1961=>[[506,3962,3959]],1962=>[[3943,763,3936]],1963=>[[387,4435]],1964=>[[66,4435]],1968=>[[3969],[0]],1966=>[[186,3968,4435,248,4437]],1967=>[[367],[0]],1969=>[[3967,203]],1973=>[[3974],[0]],1975=>[[3976],[0]],1972=>[[21,3559],[171,3559,3850,3973,3975]],1974=>[[542,3559]],1976=>[[160,3559]],1978=>[[4369,4007,3977]],1979=>[[3980],[4046]],1980=>[[3981]],1981=>[[62,3854]],1983=>[[4023,730]],2034=>[[4006],[0]],1993=>[[3994,4000,4073,4001],[205,3995,4002,4073,3988],[523,3995,4002,4073,3991],[4034,4004]],1994=>[[265],[236]],1996=>[[420,265],[609,3995]],1997=>[[3983]],2003=>[[3999],[0]],1999=>[[3997]],2004=>[[3996,4000,4073,4001],[199,265,4002,4061,4046],[3981,4003]],2005=>[[4435],[0]],2006=>[[86,4005]],2007=>[[4117,4022]],2018=>[[4009],[0]],2009=>[[209,12]],2019=>[[4011],[0]],2011=>[[637],[554]],2021=>[[4027,4021],[4027],[0]],2013=>[[4021]],2014=>[[4042,4014],[4042],[0]],2015=>[[4014]],2016=>[[4013],[4015]],2020=>[[4282,4018,17,3854,4019,4016]],2022=>[[4020],[4021]],2041=>[[420],[0]],2039=>[[265],[0]],2027=>[[4023,4479],[4028],[128,4031],[4032],[383,614,372,4030],[24],[501,128,627],[4041,265],[609,4039],[75,4469],[4281],[74,4037],[553,4038],[4033],[4035],[4036]],2028=>[[371,720]],2029=>[[3854]],2031=>[[4458],[372,4030],[4029]],2032=>[[4082]],2033=>[[707,4453]],2035=>[[4034,3981]],2036=>[[3983]],2037=>[[192],[152],[128]],2038=>[[142],[334],[128]],2042=>[[609,4039],[75,4465],[4040,376],[4041,265]],2043=>[[4440],[0]],2048=>[[4049],[0]],2056=>[[4057],[0]],2046=>[[443,4414,4043,4048,4056]],2047=>[[204],[402],[513]],2049=>[[320,4047]],2054=>[[4051],[0]],2051=>[[383,133,4058]],2055=>[[4053],[0]],2053=>[[383,614,4058]],2057=>[[383,614,4058,4054],[383,133,4058,4055]],2058=>[[4059],[506,4479],[373,3],[506,128]],2059=>[[471],[49]],2062=>[[4063,4062],[4063],[0]],2061=>[[753,4066,4062,748]],2063=>[[750,4066]],2066=>[[4435,4111,4071]],2069=>[[4070,4069],[4070],[0]],2068=>[[753,4072,4069,748]],2070=>[[750,4072]],2072=>[[4066],[3854,4071]],2073=>[[4074],[4075]],2074=>[[4068]],2075=>[[4061]],2076=>[[4077]],2077=>[[44],[488],[220]],2078=>[[4080],[4083]],2080=>[[264,4262,4450],[75,4469],[4081]],2081=>[[4082]],2082=>[[662],[661]],2083=>[[4084,4076]],2084=>[[621],[599]],2085=>[[4080],[645,401,4435]],2086=>[[4080]],2087=>[[4117,-1]],2103=>[[4136],[0]],2090=>[[4483],[0]],2146=>[[32],[0]],2126=>[[4127],[0]],2117=>[[4118,4111,4103],[4120,4090,4103],[4121,4092,4103],[37,4111],[4122],[4123,4133,4116],[60,4111,4116],[32,4111],[4124,4133,4146],[4130,4111,4146],[628,4133],[656,4111,4103],[116],[586,4106],[583,4106],[113,4106],[587],[39,4111],[4125],[293,628],[293,4126,4116],[589,4116],[580,4111,4116],[332,4116],[291,4116],[164,4460,4116],[506,4460,4116],[501],[4128],[4129]],2118=>[[249],[588],[516],[331],[31]],2131=>[[416],[0]],2120=>[[437],[146,4131]],2121=>[[195],[126],[378],[192]],2122=>[[42],[41]],2123=>[[60,633],[629]],2124=>[[358,629],[379],[361,629],[358,60,633],[361,633]],2125=>[[330],[290]],2127=>[[60,633],[629]],2128=>[[262]],2129=>[[212],[211],[411],[352],[279],[351],[412],[353]],2130=>[[361],[358,60]],2132=>[[437],[146,4131]],2133=>[[753,4134,748]],2134=>[[4453],[783]],2137=>[[4138,4137],[4138]],2136=>[[4137]],2138=>[[512],[612],[657]],2142=>[[4143],[0]],2141=>[[4145],[4147],[46],[3847,4150,4146],[32,4142]],2143=>[[3847,4150]],2145=>[[19,4146],[32,19]],2147=>[[606,4146],[32,606]],2148=>[[753,4451,748]],2149=>[[753,787,748]],2150=>[[4484],[32],[4151]],2151=>[[128]],2152=>[[4484],[4153],[4154]],2153=>[[128]],2154=>[[32]],2158=>[[4159,4158],[4159],[0]],2156=>[[4181,4158]],2159=>[[4157,4181]],2160=>[[4181,4160],[4181]],2161=>[[4160]],2175=>[[4416],[0]],2181=>[[163,4262,4404],[4184],[323,4262,4452],[344,4262,4452],[25,4262,4450],[406,4262,4463],[75,4262,4463],[4186],[4188],[24,4262,4452],[399,4262,4203],[4189,4262,4203],[4190,4262,4450],[132,4262,4450],[486,4262,4191],[608,4262,753,4175,748],[4212],[4206],[243,4262,4192],[112,139,4262,4465],[236,139,4262,4465],[572,4195,4435],[553,4196],[84,4262,4465],[264,4262,4450],[4197],[4199],[4201],[4202]],2182=>[[376],[4484]],2184=>[[721,4183,4182]],2186=>[[81,4262,4465]],2188=>[[158,4262,4465]],2189=>[[544],[545],[546]],2190=>[[61],[575]],2191=>[[128],[152],[192],[80],[442],[78]],2192=>[[373],[191],[268]],2194=>[[4262]],2195=>[[4194],[0]],2196=>[[142],[334]],2197=>[[543,592]],2199=>[[848,4262,4465]],2201=>[[849,4262,4465]],2202=>[[2350]],2203=>[[4450],[128]],2210=>[[128],[0]],2206=>[[4210,69,4262,4152]],2209=>[[4210,158,4262,4463]],2212=>[[4210,3847,4262,4150]],2217=>[[4218],[0]],2214=>[[4229],[0]],2215=>[[4237],[0]],2216=>[[405,45,4223,4217,4214,4215]],2218=>[[404,4451]],2227=>[[277],[0]],2230=>[[4234],[0]],2225=>[[4437],[0]],2223=>[[4227,265,4230,753,4225,748],[4227,220,753,3589,748],[4224,4226]],2224=>[[432],[280]],2226=>[[753,3589,748],[71,753,4225,748]],2232=>[[4233],[0]],2229=>[[561,45,4227,4231,4232]],2231=>[[220,753,3589,748],[265,4230,4440]],2233=>[[560,4451]],2234=>[[4235]],2235=>[[9,763,4451]],2238=>[[4239,4238],[4239],[0]],2237=>[[753,4243,4238,748]],2239=>[[750,4243]],2245=>[[4246],[0]],2266=>[[4263,4266],[4263],[0]],2250=>[[4251],[0]],2243=>[[405,4435,4245,4266,4250]],2244=>[[4269],[329]],2246=>[[626,273,581,4244],[626,251,4253]],2249=>[[4248,4249],[4248],[0]],2248=>[[750,4267]],2251=>[[753,4267,4249,748]],2254=>[[4255,4254],[4255],[0]],2253=>[[4269],[753,4269,4254,748]],2255=>[[750,4269]],2263=>[[572,4262,4435],[4257,163,4262,4404],[368,4262,4451],[4264,4262,4451],[4265,139,4262,4469],[75,4262,4469]],2264=>[[323],[344]],2265=>[[112],[236]],2267=>[[561,4484,4266]],2270=>[[4271,4270],[4271],[0]],2269=>[[753,4272,4270,748]],2271=>[[750,4272]],2272=>[[3589],[329]],2273=>[[130,763,4360]],2274=>[[231,174]],2275=>[[231,3848,174]],2278=>[[4279],[0]],2277=>[[4278,4280]],2279=>[[251],[397],[240]],2280=>[[4423,4283]],2281=>[[69,4152]],2283=>[[4117,4282]],2284=>[[753,4386,750,4386,748]],2287=>[[4288,4287],[4288],[0]],2286=>[[4394,4287]],2288=>[[750,4394]],2291=>[[4292,4291],[4292],[0]],2290=>[[4293,4291]],2292=>[[750,4293]],2293=>[[4377,4489,4294]],2294=>[[3559],[128]],2295=>[[3847,4150]],2296=>[[4299,4296],[4299]],2297=>[[71,4296]],2298=>[[392],[0]],2299=>[[579,45,4465],[4298,157,45,4465],[167,45,4465]],2300=>[[4302,4300],[4302]],2301=>[[278,4300]],2302=>[[4303,45,4465]],2303=>[[579],[541]],2306=>[[4307,4306],[4307],[0]],2305=>[[4360,4306]],2307=>[[750,4360]],2310=>[[4311,4310],[4311],[0]],2309=>[[4317,4310]],2311=>[[750,4317]],2314=>[[4315,4314],[4315],[0]],2313=>[[4333,4314]],2315=>[[750,4333]],2331=>[[4332],[0]],2317=>[[4360,4331]],2318=>[[406]],2328=>[[4320],[0]],2320=>[[4318]],2321=>[[45,4465]],2329=>[[4323],[0]],2323=>[[17,4466],[4321]],2326=>[[4325],[0]],2325=>[[645,4484]],2327=>[[4326,45,734,406]],2330=>[[45,4328,4465],[645,4484,4329],[4327]],2332=>[[230,4330]],2333=>[[4334,4350]],2334=>[[3118],[4360]],2342=>[[4336],[0]],2336=>[[645,4484]],2337=>[[734,406]],2338=>[[4337],[4465]],2345=>[[4341],[0]],2341=>[[17,4466,4344]],2349=>[[4347],[0]],2347=>[[4342,45,4338,4343,4344],[645,4484,4345]],2348=>[[4352]],2350=>[[230,4349],[4348],[0]],2351=>[[727,101,406]],2352=>[[141,728,406]],2353=>[[458,4465]],2357=>[[4358],[0]],2355=>[[4484,4357]],2356=>[[4484],[0]],2358=>[[746,4356],[792]],2360=>[[4355],[105,4359]],2361=>[[275,4463]],2362=>[[4361],[2765]],2363=>[[385],[380]],2364=>[[284],[375]],2365=>[[4366]],2366=>[[405,4440]],2368=>[[4449],[4442,4445]],2369=>[[4370],[4371]],2370=>[[4435]],2371=>[[4368]],2372=>[[4435]],2375=>[[4376,4375],[4376],[0]],2374=>[[753,4372,4375,748]],2376=>[[750,4372]],2377=>[[4368]],2378=>[[4377],[4382]],2379=>[[4435]],2380=>[[4368]],2383=>[[4384],[0]],2382=>[[4435,751,4383,775]],2384=>[[4435,751]],2385=>[[4435]],2386=>[[4435]],2387=>[[4442]],2388=>[[4442]],2389=>[[4442]],2390=>[[4442]],2391=>[[4442]],2392=>[[4442]],2393=>[[4442],[4449]],2394=>[[4442],[4449]],2395=>[[4435]],2396=>[[4435]],2397=>[[4435]],2398=>[[4435]],2399=>[[4442]],2400=>[[4442]],2401=>[[4435]],2402=>[[4484]],2403=>[[4484]],2404=>[[4484]],2405=>[[4442],[4449]],2406=>[[4386,4449]],2412=>[[4413],[0]],2408=>[[4435,4412]],2411=>[[4410],[0]],2410=>[[751,775]],2413=>[[751,775],[4449,4411]],2414=>[[4442],[4449]],2417=>[[4418,4417],[4418],[0]],2416=>[[4414,4417]],2418=>[[750,4414]],2421=>[[4422,4421],[4422],[0]],2420=>[[4408,4421]],2422=>[[750,4408]],2423=>[[4435]],2424=>[[4432],[4501]],2425=>[[4424]],2426=>[[4432],[4510]],2427=>[[4426]],2428=>[[4435]],2429=>[[4463]],2430=>[[4435]],2431=>[[4435]],2432=>[[4433],[4434]],2433=>[[793],[781]],2434=>[[784]],2435=>[[4432],[4493]],2438=>[[4439,4438],[4439],[0]],2437=>[[4435,4438]],2439=>[[750,4435]],2440=>[[753,4437,748]],2442=>[[4435,4445]],2446=>[[4447],[0]],2444=>[[4435,4446],[4448]],2447=>[[4449,4445]],2448=>[[4449,4449]],2449=>[[751,4435]],2450=>[[787],[786],[788],[791],[783],[785]],2451=>[[787],[786],[788],[791]],2452=>[[787],[788],[791],[783],[785]],2453=>[[787],[4454],[791],[788]],2454=>[[786]],2470=>[[794],[0]],2456=>[[4469],[4477],[4480],[4479],[4478],[4470,4457]],2457=>[[786],[782]],2458=>[[4456],[778,4450],[773,4450]],2461=>[[4462,4461],[4462],[0]],2460=>[[753,4465,4461,748]],2462=>[[750,4465]],2463=>[[790],[4464]],2464=>[[784]],2465=>[[4463],[786],[782]],2466=>[[4463],[4467]],2467=>[[786]],2468=>[[4463,4468],[4463],[0]],2469=>[[4471,4468]],2471=>[[4470,4463],[789]],2472=>[[4463]],2475=>[[4476,4475],[4476],[0]],2474=>[[4463,4475]],2476=>[[750,4463]],2477=>[[787],[788],[791],[783],[785]],2478=>[[596],[183]],2479=>[[376],[801]],2480=>[[116,4463],[586,4463],[583,4463]],2481=>[[4133],[4483]],2482=>[[4133]],2483=>[[753,787,750,787,748]],2484=>[[4435],[4463]],2485=>[[4432],[4514]],2486=>[[4426],[4463]],2487=>[[4453],[4432]],2488=>[[753,748]],2489=>[[763],[756]],2490=>[[658],[673],[214],[284],[502]],2491=>[[214,751],[284,751],[502,751]],2492=>[[658,751],[673,751],[214,751],[284,751],[502,751]],2493=>[[4497],[4498]],2494=>[[510]],2495=>[[714]],2496=>[[4501],[4516],[173],[4494],[4495]],2497=>[[4496]],2498=>[[4506],[4499],[4500],[4505],[4515]],2499=>[[173],[714],[510]],2500=>[[19],[29],[46],[47],[58],[61],[677],[75],[77],[90],[123],[147],[159],[196],[197],[219],[222],[234],[245],[267],[373],[415],[417],[455],[468],[480],[489],[512],[514],[543],[552],[597],[606],[607],[651]],2501=>[[4503],[4504]],2502=>[[4520],[170],[188],[369],[423],[427],[451],[459],[709],[565]],2503=>[[4502]],2504=>[[4506],[4505],[4515]],2505=>[[170],[188],[369],[423],[427],[451],[459],[709],[565]],2506=>[[4507],[4509]],2507=>[[3],[2],[724],[5],[660],[6],[7],[8],[9],[12],[16],[21],[812],[23],[24],[25],[26],[27],[33],[37],[40],[41],[42],[44],[675],[50],[53],[54],[56],[57],[63],[64],[65],[66],[67],[68],[70],[71],[74],[73],[76],[78],[79],[664],[80],[81],[82],[84],[85],[87],[88],[89],[91],[96],[101],[107],[111],[112],[113],[116],[122],[129],[130],[715],[132],[716],[138],[139],[140],[141],[142],[150],[151],[152],[156],[158],[160],[730],[162],[163],[848],[164],[166],[165],[168],[169],[171],[172],[680],[176],[177],[179],[180],[181],[184],[185],[189],[190],[191],[192],[682],[201],[202],[204],[208],[211],[212],[213],[713],[840],[216],[210],[841],[220],[674],[705],[225],[224],[229],[230],[233],[725],[235],[238],[844],[243],[244],[661],[250],[255],[256],[258],[259],[262],[264],[847],[268],[270],[273],[274],[279],[280],[670],[286],[288],[289],[296],[735],[298],[299],[319],[300],[729],[301],[302],[303],[304],[712],[305],[306],[307],[308],[309],[310],[312],[311],[313],[314],[316],[738],[317],[318],[736],[321],[322],[323],[324],[327],[328],[333],[334],[335],[336],[337],[340],[343],[344],[346],[348],[350],[351],[352],[353],[354],[355],[356],[357],[358],[361],[363],[702],[365],[366],[367],[368],[671],[374],[689],[377],[379],[381],[732],[728],[384],[386],[387],[719],[390],[703],[717],[690],[398],[399],[400],[401],[402],[403],[404],[406],[704],[407],[408],[409],[410],[411],[412],[413],[693],[418],[419],[421],[737],[424],[426],[425],[429],[430],[431],[434],[438],[439],[441],[846],[442],[718],[445],[446],[447],[448],[449],[452],[842],[454],[456],[460],[462],[461],[463],[466],[464],[465],[617],[695],[470],[472],[727],[473],[851],[474],[706],[476],[659],[481],[482],[483],[485],[486],[488],[490],[492],[721],[849],[722],[720],[723],[495],[496],[500],[501],[503],[508],[513],[669],[515],[517],[519],[520],[521],[813],[814],[815],[817],[816],[818],[819],[820],[821],[822],[823],[824],[825],[826],[829],[828],[830],[831],[833],[832],[834],[827],[835],[522],[836],[837],[838],[839],[528],[529],[530],[532],[535],[538],[707],[540],[542],[544],[545],[546],[547],[553],[556],[557],[558],[559],[560],[561],[566],[567],[568],[571],[572],[575],[576],[577],[578],[580],[581],[708],[697],[584],[585],[583],[586],[845],[592],[593],[598],[599],[698],[601],[602],[603],[604],[610],[613],[615],[618],[619],[625],[627],[631],[711],[636],[662],[638],[639],[640],[641],[646],[647],[648],[650],[652],[653],[656],[843]],2508=>[[731],[741],[735],[738],[736],[733],[744],[740],[737],[734],[739],[742],[743],[583],[586]],2509=>[[4508]],2510=>[[4512],[4513]],2511=>[[4520],[4516]],2512=>[[4511]],2513=>[[4506],[4500],[4515]],2514=>[[4506],[4499],[4500],[4505]],2515=>[[214],[284],[658],[673],[502]],2516=>[[4517],[4518],[4519]],2517=>[[2],[19],[12],[27],[29],[46],[47],[58],[61],[677],[66],[75],[77],[90],[123],[147],[159],[196],[197],[201],[210],[219],[222],[224],[245],[661],[267],[373],[387],[390],[398],[401],[413],[415],[417],[452],[455],[468],[470],[659],[480],[489],[720],[721],[722],[723],[496],[503],[512],[519],[514],[520],[543],[552],[597],[606],[607],[615],[662],[648],[651]],2518=>[[510]],2519=>[[234]],2520=>[[4521],[4522],[4524],[4526],[4527]],2521=>[[3],[724],[5],[6],[7],[8],[9],[13],[16],[21],[22],[24],[23],[25],[26],[33],[37],[40],[42],[41],[44],[675],[50],[53],[54],[56],[57],[63],[65],[64],[67],[68],[70],[73],[74],[71],[76],[78],[79],[664],[80],[81],[82],[84],[85],[87],[89],[88],[91],[93],[96],[101],[107],[112],[111],[113],[116],[122],[129],[130],[132],[136],[716],[138],[139],[140],[141],[142],[150],[151],[152],[158],[160],[164],[163],[162],[165],[166],[168],[169],[171],[680],[176],[179],[180],[181],[185],[184],[682],[202],[156],[204],[189],[190],[191],[192],[208],[212],[211],[213],[216],[214],[220],[674],[705],[225],[229],[230],[233],[250],[235],[238],[244],[725],[255],[256],[258],[259],[243],[262],[850],[264],[268],[270],[273],[274],[279],[280],[284],[670],[286],[288],[289],[323],[316],[319],[300],[304],[301],[302],[318],[303],[712],[306],[298],[305],[299],[314],[308],[307],[317],[309],[310],[311],[312],[313],[296],[321],[322],[325],[324],[327],[328],[333],[334],[335],[336],[337],[340],[343],[344],[348],[346],[350],[351],[352],[353],[354],[355],[357],[356],[358],[361],[363],[702],[365],[367],[366],[374],[368],[689],[671],[377],[379],[381],[728],[382],[384],[719],[703],[717],[690],[399],[400],[402],[403],[404],[406],[704],[407],[409],[410],[408],[411],[412],[693],[418],[419],[708],[421],[424],[425],[426],[429],[430],[431],[434],[438],[439],[441],[440],[442],[445],[446],[447],[448],[449],[676],[454],[456],[460],[461],[462],[463],[464],[465],[466],[617],[695],[472],[727],[473],[474],[706],[476],[481],[482],[483],[485],[486],[488],[490],[492],[495],[501],[500],[502],[508],[513],[669],[515],[517],[521],[522],[528],[529],[530],[533],[532],[535],[538],[707],[540],[542],[544],[545],[546],[547],[553],[556],[557],[558],[559],[561],[560],[565],[566],[567],[568],[576],[571],[575],[572],[577],[578],[580],[581],[697],[592],[593],[583],[584],[585],[586],[598],[599],[600],[698],[601],[602],[604],[603],[610],[613],[618],[619],[631],[711],[636],[627],[639],[638],[640],[647],[641],[650],[652],[653],[656]],2522=>[[510]],2523=>[[99],[234],[206],[484],[487]],2524=>[[4523]],2525=>[[172],[177],[386],[565],[625],[646]],2526=>[[4525]],2527=>[[660]]]];
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 6572b6b..6768e98 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1513,6 +1513,14 @@ private function translate( $ast ) {
return null;
}
return $this->translate_sequence( $ast->get_children() );
+ case 'insertUpdateList':
+ // Translate "ON DUPLICATE KEY UPDATE" to "ON CONFLICT DO UPDATE SET".
+ return sprintf(
+ 'ON CONFLICT DO UPDATE SET %s',
+ $this->translate( $ast->get_child_node( 'updateList' ) )
+ );
+ case 'simpleExpr':
+ return $this->translate_simple_expr( $ast );
case 'predicateOperations':
$token = $ast->get_child_token();
if ( WP_MySQL_Lexer::LIKE_SYMBOL === $token->id ) {
@@ -1601,6 +1609,20 @@ private function translate_sequence( array $nodes, string $separator = ' ' ): ?s
return implode( $separator, $parts );
}
+ private function translate_simple_expr( WP_Parser_Node $node ): string {
+ $token = $node->get_child_token();
+
+ // Translate "VALUES(col)" to "excluded.col" in ON DUPLICATE KEY UPDATE.
+ if ( null !== $token && WP_MySQL_Lexer::VALUES_SYMBOL === $token->id ) {
+ return sprintf(
+ '"excluded".%s',
+ $this->translate( $node->get_child_node( 'simpleIdentifier' ) )
+ );
+ }
+
+ return $this->translate_sequence( $node->get_children() );
+ }
+
private function translate_like( WP_Parser_Node $node ): string {
$tokens = $node->get_descendant_tokens();
$is_binary = isset( $tokens[1] ) && WP_MySQL_Lexer::BINARY_SYMBOL === $tokens[1]->id;
@@ -1893,6 +1915,13 @@ private function get_sqlite_create_table_statement( string $table_name, ?string
}
$sql .= ' ' . $type;
+
+ // In MySQL, text fields are case-insensitive by default.
+ // COLLATE NOCASE emulates the same behavior in SQLite.
+ // @TODO: Respect the actual column and index collation.
+ if ( 'TEXT' === $type ) {
+ $sql .= ' COLLATE NOCASE';
+ }
if ( 'NO' === $column['IS_NULLABLE'] ) {
$sql .= ' NOT NULL';
}
From 94fa13488cb755641b74fd17d8128ff53df6f3e1 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 16 Jan 2025 13:11:32 +0100
Subject: [PATCH 046/124] Implement INSERT IGNORE and UPDATE IGNORE
---
.../sqlite-ast/class-wp-sqlite-driver.php | 55 ++++++++++++++-----
1 file changed, 40 insertions(+), 15 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 6768e98..3002347 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -905,6 +905,8 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$this->execute_select_statement( $ast );
break;
case 'insertStatement':
+ $this->execute_insert_statement( $ast );
+ break;
case 'updateStatement':
$this->execute_update_statement( $ast );
break;
@@ -1061,6 +1063,20 @@ private function execute_select_statement( WP_Parser_Node $node ): void {
);
}
+ private function execute_insert_statement( WP_Parser_Node $node ): void {
+ $parts = array();
+ foreach ( $node->get_children() as $child ) {
+ if ( $child instanceof WP_MySQL_Token && WP_MySQL_Lexer::IGNORE_SYMBOL === $child->id ) {
+ $parts[] = 'OR IGNORE';
+ } else {
+ $parts[] = $this->translate( $child );
+ }
+ }
+ $query = implode( ' ', $parts );
+ $this->execute_sqlite_query( $query );
+ $this->set_result_from_affected_rows();
+ }
+
private function execute_update_statement( WP_Parser_Node $node ): void {
// @TODO: Add support for UPDATE with multiple tables and JOINs.
// SQLite supports them in the FROM clause.
@@ -1077,8 +1093,9 @@ private function execute_update_statement( WP_Parser_Node $node ): void {
* Will be rewritten to:
* UPDATE t SET c = 1 WHERE rowid IN ( SELECT rowid FROM t WHERE c = 2 LIMIT 1 );
*/
+ $where_subquery = null;
if ( $has_order || $has_limit ) {
- $subquery = 'SELECT rowid FROM ' . $this->translate_sequence(
+ $where_subquery = 'SELECT rowid FROM ' . $this->translate_sequence(
array(
$node->get_descendant_node( 'tableReference' ),
$node->get_descendant_node( 'whereClause' ),
@@ -1086,23 +1103,31 @@ private function execute_update_statement( WP_Parser_Node $node ): void {
$node->get_descendant_node( 'simpleLimitClause' ),
)
);
+ }
- $update_nodes = array();
- foreach ( $node->get_children() as $child ) {
- $update_nodes[] = $child;
- if (
- $child instanceof WP_Parser_Node
- && 'updateList' === $child->rule_name
- ) {
- // Skip WHERE, ORDER BY, and LIMIT.
- break;
- }
+ $parts = array();
+ foreach ( $node->get_children() as $child ) {
+ if ( $child instanceof WP_MySQL_Token && WP_MySQL_Lexer::IGNORE_SYMBOL === $child->id ) {
+ $parts[] = 'OR IGNORE';
+ } else {
+ $parts[] = $this->translate( $child );
+ }
+
+ if (
+ null !== $where_subquery
+ && $child instanceof WP_Parser_Node
+ && 'updateList' === $child->rule_name
+ ) {
+ // Skip WHERE, ORDER BY, and LIMIT.
+ break;
}
- $query = $this->translate_sequence( $update_nodes )
- . ' WHERE rowid IN ( ' . $subquery . ' )';
- } else {
- $query = $this->translate( $node );
}
+
+ $query = implode( ' ', $parts );
+ if ( null !== $where_subquery ) {
+ $query .= ' WHERE rowid IN ( ' . $where_subquery . ' )';
+ }
+
$this->execute_sqlite_query( $query );
$this->set_result_from_affected_rows();
}
From f8af977dda65e1210dc36350ae50c480ff17858c Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 16 Jan 2025 16:36:56 +0100
Subject: [PATCH 047/124] Implement DROP TABLE in information schema
---
...s-wp-sqlite-information-schema-builder.php | 35 +++++++++++++++++++
1 file changed, 35 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index ca0302a..a5475f3 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -505,6 +505,41 @@ public function record_alter_table( WP_Parser_Node $node ): void {
}
}
+ public function record_drop_table( WP_Parser_Node $node ): void {
+ $child_node = $node->get_child_node();
+ if ( $child_node->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL ) ) {
+ return;
+ }
+
+ $table_refs = $child_node->get_child_node( 'tableRefList' )->get_child_nodes();
+ foreach ( $table_refs as $table_ref ) {
+ $table_name = $this->get_value( $table_ref );
+ $this->delete_values(
+ '_mysql_information_schema_tables',
+ array(
+ 'table_schema' => $this->db_name,
+ 'table_name' => $table_name,
+ )
+ );
+ $this->delete_values(
+ '_mysql_information_schema_columns',
+ array(
+ 'table_schema' => $this->db_name,
+ 'table_name' => $table_name,
+ )
+ );
+ $this->delete_values(
+ '_mysql_information_schema_statistics',
+ array(
+ 'table_schema' => $this->db_name,
+ 'table_name' => $table_name,
+ )
+ );
+ }
+
+ // @TODO: RESTRICT vs. CASCADE
+ }
+
private function record_add_column( string $table_name, string $column_name, WP_Parser_Node $node ): void {
$position = $this->query(
'SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = ?',
From 28bb50d057f947b0de369ada5fcfb3dba2c9b5ee Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 16 Jan 2025 16:51:39 +0100
Subject: [PATCH 048/124] Implement SHOW TABLE STATUS
---
tests/WP_SQLite_Driver_Tests.php | 42 ++++++++++
.../sqlite-ast/class-wp-sqlite-driver.php | 76 +++++++++++++++++++
2 files changed, 118 insertions(+)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index c33d3b8..37a9341 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -768,6 +768,14 @@ public function testShowTableStatusLike() {
);"
);
+ $this->assertQuery(
+ "CREATE TABLE _another_table (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name TEXT NOT NULL default '',
+ option_value TEXT NOT NULL default ''
+ );"
+ );
+
$this->assertQuery(
"SHOW TABLE STATUS LIKE '_tmp_table%';"
);
@@ -781,6 +789,40 @@ public function testShowTableStatusLike() {
);
}
+ public function testShowTableStatusWhere() {
+ // Created in setUp() function
+ $this->assertQuery( 'DROP TABLE _options' );
+ $this->assertQuery( 'DROP TABLE _dates' );
+
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table1 (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name TEXT NOT NULL default '',
+ option_value TEXT NOT NULL default ''
+ );"
+ );
+
+ $this->assertQuery(
+ "CREATE TABLE _tmp_table2 (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
+ option_name TEXT NOT NULL default '',
+ option_value TEXT NOT NULL default ''
+ );"
+ );
+
+ $this->assertQuery(
+ "SHOW TABLE STATUS WHERE SUBSTR(table_name, 11, 1) = '1'"
+ );
+ $this->assertCount(
+ 1,
+ $this->engine->get_query_results()
+ );
+ $this->assertEquals(
+ '_tmp_table1',
+ $this->engine->get_query_results()[0]->Name
+ );
+ }
+
public function testCreateTable() {
$result = $this->assertQuery(
"CREATE TABLE wptests_users (
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 3002347..03683c6 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1334,6 +1334,7 @@ private function execute_drop_table_statement( WP_Parser_Node $node ): void {
$this->execute_sqlite_query( $query );
}
$this->results = $this->last_exec_returned;
+ $this->information_schema_builder->record_drop_table( $node );
}
private function execute_show_statement( WP_Parser_Node $node ): void {
@@ -1380,6 +1381,9 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
)
);
return;
+ case WP_MySQL_Lexer::TABLE_SYMBOL:
+ $this->execute_show_table_status_statement( $node );
+ break;
default:
// @TODO
}
@@ -1414,6 +1418,58 @@ private function execute_show_index_statement( string $table_name ): void {
$this->set_results_from_fetched_data( $index_info );
}
+ private function execute_show_table_status_statement( WP_Parser_Node $node ): void {
+ // FROM/IN database.
+ $in_db = $node->get_child_node( 'idDb' );
+ $database = null === $in_db ? $this->db_name : $this->translate( $in_db );
+
+ // LIKE and WHERE clauses.
+ $like_or_where = $node->get_child_node( 'likeOrWhere' );
+ if ( null !== $like_or_where ) {
+ $condition = $this->get_show_like_or_where_condition( $like_or_where );
+ }
+
+ // Fetch table information.
+ $table_info = $this->execute_sqlite_query(
+ sprintf(
+ 'SELECT * FROM _mysql_information_schema_tables WHERE table_schema = ? %s',
+ $condition ?? ''
+ ),
+ array( $database )
+ )->fetchAll( PDO::FETCH_ASSOC );
+
+ if ( false === $table_info ) {
+ $this->set_results_from_fetched_data( array() );
+ }
+
+ // Format the results.
+ $tables = array();
+ foreach ( $table_info as $value ) {
+ $tables[] = (object) array(
+ 'Name' => $value['TABLE_NAME'],
+ 'Engine' => $value['ENGINE'],
+ 'Version' => $value['VERSION'],
+ 'Row_format' => $value['ROW_FORMAT'],
+ 'Rows' => $value['TABLE_ROWS'],
+ 'Avg_row_length' => $value['AVG_ROW_LENGTH'],
+ 'Data_length' => $value['DATA_LENGTH'],
+ 'Max_data_length' => $value['MAX_DATA_LENGTH'],
+ 'Index_length' => $value['INDEX_LENGTH'],
+ 'Data_free' => $value['DATA_FREE'],
+ 'Auto_increment' => $value['AUTO_INCREMENT'],
+ 'Create_time' => $value['CREATE_TIME'],
+ 'Update_time' => $value['UPDATE_TIME'],
+ 'Check_time' => $value['CHECK_TIME'],
+ 'Collation' => $value['TABLE_COLLATION'],
+ 'Checksum' => $value['CHECKSUM'],
+ 'Create_options' => $value['CREATE_OPTIONS'],
+ 'Comment' => $value['TABLE_COMMENT'],
+ );
+ }
+
+ $this->set_results_from_fetched_data( $tables );
+ }
+
private function execute_describe_statement( WP_Parser_Node $node ): void {
$table_name = $this->unquote_sqlite_identifier(
$this->translate( $node->get_child_node( 'tableRef' ) )
@@ -2125,6 +2181,26 @@ function ( $column ) {
return $sql;
}
+ private function get_show_like_or_where_condition( WP_Parser_Node $like_or_where ): string {
+ $like_clause = $like_or_where->get_child_node( 'likeClause' );
+ if ( null !== $like_clause ) {
+ $value = $this->translate(
+ $like_clause->get_child_node( 'textStringLiteral' )
+ );
+ return sprintf( "AND table_name LIKE %s ESCAPE '\\'", $value );
+ }
+
+ $where_clause = $like_or_where->get_child_node( 'whereClause' );
+ if ( null !== $where_clause ) {
+ $value = $this->translate(
+ $where_clause->get_child_node( 'expr' )
+ );
+ return sprintf( 'AND %s', $value );
+ }
+
+ return '';
+ }
+
private function unquote_sqlite_identifier( string $quoted_identifier ): string {
$first_byte = $quoted_identifier[0] ?? null;
if ( '"' === $first_byte ) {
From 13cd58fa8596621efa1150321b88b62039d21894 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 17 Jan 2025 09:31:54 +0100
Subject: [PATCH 049/124] Implement SHOW TABLES
---
tests/WP_SQLite_Driver_Tests.php | 15 +++++-
.../sqlite-ast/class-wp-sqlite-driver.php | 46 +++++++++++++++++++
2 files changed, 60 insertions(+), 1 deletion(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 37a9341..e99871b 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -664,7 +664,20 @@ public function testShowTablesLike() {
$this->assertEquals(
array(
(object) array(
- 'Tables_in_db' => '_tmp_table',
+ 'Tables_in_wp' => '_tmp_table',
+ ),
+ ),
+ $this->engine->get_query_results()
+ );
+
+ $this->assertQuery(
+ "SHOW FULL TABLES LIKE '_tmp_table';"
+ );
+ $this->assertEquals(
+ array(
+ (object) array(
+ 'Tables_in_wp' => '_tmp_table',
+ 'Table_type' => 'BASE TABLE',
),
),
$this->engine->get_query_results()
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 03683c6..a0b40f0 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1384,6 +1384,9 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
case WP_MySQL_Lexer::TABLE_SYMBOL:
$this->execute_show_table_status_statement( $node );
break;
+ case WP_MySQL_Lexer::TABLES_SYMBOL:
+ $this->execute_show_tables_statement( $node );
+ break;
default:
// @TODO
}
@@ -1470,6 +1473,49 @@ private function execute_show_table_status_statement( WP_Parser_Node $node ): vo
$this->set_results_from_fetched_data( $tables );
}
+ private function execute_show_tables_statement( WP_Parser_Node $node ): void {
+ // FROM/IN database.
+ $in_db = $node->get_child_node( 'idDb' );
+ $database = null === $in_db ? $this->db_name : $this->translate( $in_db );
+
+ // LIKE and WHERE clauses.
+ $like_or_where = $node->get_child_node( 'likeOrWhere' );
+ if ( null !== $like_or_where ) {
+ $condition = $this->get_show_like_or_where_condition( $like_or_where );
+ }
+
+ // Fetch table information.
+ $table_info = $this->execute_sqlite_query(
+ sprintf(
+ 'SELECT * FROM _mysql_information_schema_tables WHERE table_schema = ? %s',
+ $condition ?? ''
+ ),
+ array( $database )
+ )->fetchAll( PDO::FETCH_ASSOC );
+
+ if ( false === $table_info ) {
+ $this->set_results_from_fetched_data( array() );
+ }
+
+ // Handle the FULL keyword.
+ $command_type = $node->get_child_node( 'showCommandType' );
+ $is_full = $command_type && $command_type->has_child_token( WP_MySQL_Lexer::FULL_SYMBOL );
+
+ // Format the results.
+ $tables = array();
+ foreach ( $table_info as $value ) {
+ $table = array(
+ "Tables_in_$database" => $value['TABLE_NAME'],
+ );
+ if ( true === $is_full ) {
+ $table['Table_type'] = $value['TABLE_TYPE'];
+ }
+ $tables[] = (object) $table;
+ }
+
+ $this->set_results_from_fetched_data( $tables );
+ }
+
private function execute_describe_statement( WP_Parser_Node $node ): void {
$table_name = $this->unquote_sqlite_identifier(
$this->translate( $node->get_child_node( 'tableRef' ) )
From eecc39b255f3fb801608a302e78d0ea8ea8ac227 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 17 Jan 2025 10:06:14 +0100
Subject: [PATCH 050/124] Fix saving and displaying index info
---
tests/WP_SQLite_Driver_Tests.php | 97 ++++++++-----------
.../sqlite-ast/class-wp-sqlite-driver.php | 4 +
...s-wp-sqlite-information-schema-builder.php | 19 ++--
3 files changed, 56 insertions(+), 64 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index e99871b..af32eae 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -2642,12 +2642,13 @@ public function testShowIndex() {
object_id bigint(20) unsigned NOT NULL default 0,
term_taxonomy_id bigint(20) unsigned NOT NULL default 0,
term_name varchar(11) NOT NULL default 0,
- FULLTEXT KEY term_name_fulltext (term_name),
+ geom_col geometry NOT NULL,
+ FULLTEXT KEY term_name_fulltext1 (term_name),
FULLTEXT INDEX term_name_fulltext2 (`term_name`),
- SPATIAL KEY term_name_spatial (term_name),
+ SPATIAL KEY geom_col_spatial (geom_col),
PRIMARY KEY (object_id,term_taxonomy_id),
KEY term_taxonomy_id (term_taxonomy_id),
- KEY compound_key (object_id(20),term_taxonomy_id(20))
+ KEY compound_key (object_id,term_taxonomy_id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci'
);
$this->assertEquals( '', $this->engine->get_error_message() );
@@ -2662,7 +2663,7 @@ public function testShowIndex() {
'Table' => 'wptests_term_relationships',
'Non_unique' => '0',
'Key_name' => 'PRIMARY',
- 'Seq_in_index' => '0',
+ 'Seq_in_index' => '1',
'Column_name' => 'object_id',
'Collation' => 'A',
'Cardinality' => '0',
@@ -2672,12 +2673,14 @@ public function testShowIndex() {
'Index_type' => 'BTREE',
'Comment' => '',
'Index_comment' => '',
+ 'Visible' => 'YES',
+ 'Expression' => null,
),
(object) array(
'Table' => 'wptests_term_relationships',
'Non_unique' => '0',
'Key_name' => 'PRIMARY',
- 'Seq_in_index' => '0',
+ 'Seq_in_index' => '2',
'Column_name' => 'term_taxonomy_id',
'Collation' => 'A',
'Cardinality' => '0',
@@ -2687,27 +2690,31 @@ public function testShowIndex() {
'Index_type' => 'BTREE',
'Comment' => '',
'Index_comment' => '',
+ 'Visible' => 'YES',
+ 'Expression' => null,
),
(object) array(
'Table' => 'wptests_term_relationships',
'Non_unique' => '1',
- 'Key_name' => 'compound_key',
- 'Seq_in_index' => '0',
- 'Column_name' => 'object_id',
+ 'Key_name' => 'geom_col_spatial',
+ 'Seq_in_index' => '1',
+ 'Column_name' => 'geom_col',
'Collation' => 'A',
'Cardinality' => '0',
- 'Sub_part' => null,
+ 'Sub_part' => 32,
'Packed' => null,
'Null' => '',
- 'Index_type' => 'BTREE',
+ 'Index_type' => 'SPATIAL',
'Comment' => '',
'Index_comment' => '',
+ 'Visible' => 'YES',
+ 'Expression' => null,
),
(object) array(
'Table' => 'wptests_term_relationships',
'Non_unique' => '1',
- 'Key_name' => 'compound_key',
- 'Seq_in_index' => '0',
+ 'Key_name' => 'term_taxonomy_id',
+ 'Seq_in_index' => '1',
'Column_name' => 'term_taxonomy_id',
'Collation' => 'A',
'Cardinality' => '0',
@@ -2717,13 +2724,15 @@ public function testShowIndex() {
'Index_type' => 'BTREE',
'Comment' => '',
'Index_comment' => '',
+ 'Visible' => 'YES',
+ 'Expression' => null,
),
(object) array(
'Table' => 'wptests_term_relationships',
'Non_unique' => '1',
- 'Key_name' => 'term_taxonomy_id',
- 'Seq_in_index' => '0',
- 'Column_name' => 'term_taxonomy_id',
+ 'Key_name' => 'compound_key',
+ 'Seq_in_index' => '1',
+ 'Column_name' => 'object_id',
'Collation' => 'A',
'Cardinality' => '0',
'Sub_part' => null,
@@ -2732,29 +2741,33 @@ public function testShowIndex() {
'Index_type' => 'BTREE',
'Comment' => '',
'Index_comment' => '',
+ 'Visible' => 'YES',
+ 'Expression' => null,
),
(object) array(
'Table' => 'wptests_term_relationships',
'Non_unique' => '1',
- 'Key_name' => 'term_name_spatial',
- 'Seq_in_index' => '0',
- 'Column_name' => 'term_name',
+ 'Key_name' => 'compound_key',
+ 'Seq_in_index' => '2',
+ 'Column_name' => 'term_taxonomy_id',
'Collation' => 'A',
'Cardinality' => '0',
'Sub_part' => null,
'Packed' => null,
'Null' => '',
- 'Index_type' => 'SPATIAL',
+ 'Index_type' => 'BTREE',
'Comment' => '',
'Index_comment' => '',
+ 'Visible' => 'YES',
+ 'Expression' => null,
),
(object) array(
'Table' => 'wptests_term_relationships',
'Non_unique' => '1',
- 'Key_name' => 'term_name_fulltext2',
- 'Seq_in_index' => '0',
+ 'Key_name' => 'term_name_fulltext1',
+ 'Seq_in_index' => '1',
'Column_name' => 'term_name',
- 'Collation' => 'A',
+ 'Collation' => null,
'Cardinality' => '0',
'Sub_part' => null,
'Packed' => null,
@@ -2762,14 +2775,16 @@ public function testShowIndex() {
'Index_type' => 'FULLTEXT',
'Comment' => '',
'Index_comment' => '',
+ 'Visible' => 'YES',
+ 'Expression' => null,
),
(object) array(
'Table' => 'wptests_term_relationships',
'Non_unique' => '1',
- 'Key_name' => 'term_name_fulltext',
- 'Seq_in_index' => '0',
+ 'Key_name' => 'term_name_fulltext2',
+ 'Seq_in_index' => '1',
'Column_name' => 'term_name',
- 'Collation' => 'A',
+ 'Collation' => null,
'Cardinality' => '0',
'Sub_part' => null,
'Packed' => null,
@@ -2777,36 +2792,8 @@ public function testShowIndex() {
'Index_type' => 'FULLTEXT',
'Comment' => '',
'Index_comment' => '',
- ),
- (object) array(
- 'Table' => 'wptests_term_relationships',
- 'Non_unique' => '0',
- 'Key_name' => 'wptests_term_relationships',
- 'Seq_in_index' => '0',
- 'Column_name' => 'object_id',
- 'Collation' => 'A',
- 'Cardinality' => '0',
- 'Sub_part' => null,
- 'Packed' => null,
- 'Null' => '',
- 'Index_type' => 'BTREE',
- 'Comment' => '',
- 'Index_comment' => '',
- ),
- (object) array(
- 'Table' => 'wptests_term_relationships',
- 'Non_unique' => '0',
- 'Key_name' => 'wptests_term_relationships',
- 'Seq_in_index' => '0',
- 'Column_name' => 'term_taxonomy_id',
- 'Collation' => 'A',
- 'Cardinality' => '0',
- 'Sub_part' => null,
- 'Packed' => null,
- 'Null' => '',
- 'Index_type' => 'BTREE',
- 'Comment' => '',
- 'Index_comment' => '',
+ 'Visible' => 'YES',
+ 'Expression' => null,
),
),
$this->engine->get_query_results()
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index a0b40f0..6444b62 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1414,6 +1414,10 @@ private function execute_show_index_statement( string $table_name ): void {
FROM _mysql_information_schema_statistics
WHERE table_schema = ?
AND table_name = ?
+ ORDER BY
+ INDEX_NAME = "PRIMARY" DESC,
+ INDEX_TYPE = "FULLTEXT" ASC,
+ SEQ_IN_INDEX
',
array( $this->db_name, $table_name )
)->fetchAll( PDO::FETCH_OBJ );
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index a5475f3..bd45f7a 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -683,15 +683,7 @@ private function record_add_constraint( string $table_name, WP_Parser_Node $node
$column_info
);
- // Get first index column data type (needed for index type).
- $first_column_name = $column_info[0]['COLUMN_NAME'];
- $first_column_type = $column_info_map[ $first_column_name ]['DATA_TYPE'] ?? null;
- $has_spatial_column = null !== $first_column_type && $this->is_spatial_data_type( $first_column_type );
-
- $non_unique = $this->get_index_non_unique( $keyword );
- $index_name = $this->get_index_name( $node );
- $index_type = $this->get_index_type( $node, $keyword, $has_spatial_column );
-
+ // Get key parts.
$key_list = $node->get_child_node( 'keyListVariants' )->get_child();
if ( 'keyListWithExpression' === $key_list->rule_name ) {
$key_parts = array();
@@ -702,6 +694,15 @@ private function record_add_constraint( string $table_name, WP_Parser_Node $node
$key_parts = $key_list->get_descendant_nodes( 'keyPart' );
}
+ // Get first index column data type (needed for index type).
+ $first_column_name = $this->get_index_column_name( $key_parts[0] );
+ $first_column_type = $column_info_map[ $first_column_name ]['DATA_TYPE'] ?? null;
+ $has_spatial_column = null !== $first_column_type && $this->is_spatial_data_type( $first_column_type );
+
+ $non_unique = $this->get_index_non_unique( $keyword );
+ $index_name = $this->get_index_name( $node );
+ $index_type = $this->get_index_type( $node, $keyword, $has_spatial_column );
+
$seq_in_index = 1;
foreach ( $key_parts as $key_part ) {
$column_name = $this->get_index_column_name( $key_part );
From b8fc7d826b1e4e279090ab0922ef88343fc17895 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 17 Jan 2025 11:30:30 +0100
Subject: [PATCH 051/124] Implement ON UPDATE CURRENT_TIMESTAMP
---
tests/WP_SQLite_Driver_Tests.php | 2 +-
.../sqlite-ast/class-wp-sqlite-driver.php | 73 ++++++++++++-------
2 files changed, 49 insertions(+), 26 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index af32eae..496329c 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -1293,7 +1293,7 @@ public function testColumnWithOnUpdate() {
'Type' => 'int(11)',
'Null' => 'NO',
'Key' => '',
- 'Default' => '0',
+ 'Default' => null,
'Extra' => '',
),
(object) array(
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 6444b62..e76ffec 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -2014,10 +2014,11 @@ private function get_sqlite_create_table_statement( string $table_name, ?string
// 4. Generate CREATE TABLE statement columns.
$rows = array();
+ $on_update_queries = array();
$has_autoincrement = false;
foreach ( $column_info as $column ) {
- $sql = ' ';
- $sql .= sprintf( '"%s"', str_replace( '"', '""', $column['COLUMN_NAME'] ) );
+ $query = ' ';
+ $query .= sprintf( '"%s"', str_replace( '"', '""', $column['COLUMN_NAME'] ) );
$type = self::DATA_TYPE_STRING_MAP[ $column['DATA_TYPE'] ];
@@ -2045,30 +2046,37 @@ private function get_sqlite_create_table_statement( string $table_name, ?string
$type = 'INT';
}
- $sql .= ' ' . $type;
+ $query .= ' ' . $type;
// In MySQL, text fields are case-insensitive by default.
// COLLATE NOCASE emulates the same behavior in SQLite.
// @TODO: Respect the actual column and index collation.
if ( 'TEXT' === $type ) {
- $sql .= ' COLLATE NOCASE';
+ $query .= ' COLLATE NOCASE';
}
if ( 'NO' === $column['IS_NULLABLE'] ) {
- $sql .= ' NOT NULL';
+ $query .= ' NOT NULL';
}
if ( 'auto_increment' === $column['EXTRA'] ) {
$has_autoincrement = true;
- $sql .= ' PRIMARY KEY AUTOINCREMENT';
+ $query .= ' PRIMARY KEY AUTOINCREMENT';
}
if ( null !== $column['COLUMN_DEFAULT'] ) {
// @TODO: Correctly quote based on the data type.
- $sql .= ' DEFAULT ' . $this->pdo->quote( $column['COLUMN_DEFAULT'] );
+ $query .= ' DEFAULT ' . $this->pdo->quote( $column['COLUMN_DEFAULT'] );
+ }
+ $rows[] = $query;
+
+ if ( 'on update CURRENT_TIMESTAMP' === $column['EXTRA'] ) {
+ $on_update_queries[] = $this->get_column_on_update_trigger_query(
+ $table_name,
+ $column['COLUMN_NAME']
+ );
}
- $rows[] = $sql;
}
- // 4. Generate CREATE TABLE statement constraints, collect indexes.
- $create_index_sqls = array();
+ // 5. Generate CREATE TABLE statement constraints, collect indexes.
+ $create_index_queries = array();
foreach ( $grouped_constraints as $constraint ) {
ksort( $constraint );
$info = $constraint[1];
@@ -2077,8 +2085,8 @@ private function get_sqlite_create_table_statement( string $table_name, ?string
if ( $has_autoincrement ) {
continue;
}
- $sql = ' PRIMARY KEY (';
- $sql .= implode(
+ $query = ' PRIMARY KEY (';
+ $query .= implode(
', ',
array_map(
function ( $column ) {
@@ -2087,15 +2095,15 @@ function ( $column ) {
$constraint
)
);
- $sql .= ')';
- $rows[] = $sql;
+ $query .= ')';
+ $rows[] = $query;
} else {
$is_unique = '0' === $info['NON_UNIQUE'];
- $sql = sprintf( 'CREATE %sINDEX', $is_unique ? 'UNIQUE ' : '' );
- $sql .= sprintf( ' "%s"', $info['INDEX_NAME'] );
- $sql .= sprintf( ' ON "%s" (', $table_name );
- $sql .= implode(
+ $query = sprintf( 'CREATE %sINDEX', $is_unique ? 'UNIQUE ' : '' );
+ $query .= sprintf( ' "%s"', $info['INDEX_NAME'] );
+ $query .= sprintf( ' ON "%s" (', $table_name );
+ $query .= implode(
', ',
array_map(
function ( $column ) {
@@ -2104,17 +2112,17 @@ function ( $column ) {
$constraint
)
);
- $sql .= ')';
+ $query .= ')';
- $create_index_sqls[] = $sql;
+ $create_index_queries[] = $query;
}
}
- // 5. Compose the CREATE TABLE statement.
- $sql = sprintf( 'CREATE TABLE "%s" (%s', str_replace( '"', '""', $new_table_name ?? $table_name ), "\n" );
- $sql .= implode( ",\n", $rows );
- $sql .= "\n)";
- return array_merge( array( $sql ), $create_index_sqls );
+ // 6. Compose the CREATE TABLE statement.
+ $create_table_query = sprintf( 'CREATE TABLE "%s" (%s', str_replace( '"', '""', $new_table_name ?? $table_name ), "\n" );
+ $create_table_query .= implode( ",\n", $rows );
+ $create_table_query .= "\n)";
+ return array_merge( array( $create_table_query ), $create_index_queries, $on_update_queries );
}
private function get_mysql_create_table_statement( string $table_name ): ?string {
@@ -2251,6 +2259,21 @@ private function get_show_like_or_where_condition( WP_Parser_Node $like_or_where
return '';
}
+ private function get_column_on_update_trigger_query( string $table, string $column ): string {
+ // The trigger wouldn't work for virtual and "WITHOUT ROWID" tables,
+ // but currently that can't happen as we're not creating such tables.
+ // See: https://www.sqlite.org/rowidtable.html
+ $trigger_name = "__{$table}_{$column}_on_update__";
+ return "
+ CREATE TRIGGER \"$trigger_name\"
+ AFTER UPDATE ON \"$table\"
+ FOR EACH ROW
+ BEGIN
+ UPDATE \"$table\" SET \"$column\" = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;
+ END
+ ";
+ }
+
private function unquote_sqlite_identifier( string $quoted_identifier ): string {
$first_byte = $quoted_identifier[0] ?? null;
if ( '"' === $first_byte ) {
From ff8afed5b273390a10a9137bc096c5cff887c7df Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 17 Jan 2025 11:42:53 +0100
Subject: [PATCH 052/124] Prefix index names in SQLite to prevent conflicts
---
tests/WP_SQLite_Driver_Tests.php | 2 +-
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 496329c..3d63ef8 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -380,7 +380,7 @@ public function testShowCreateTableWithAlterAndCreateIndex() {
);
}
- public function testCreateTablseWithIdenticalIndexNames() {
+ public function testCreateTablesWithIdenticalIndexNames() {
$this->assertQuery(
"CREATE TABLE _tmp_table_a (
ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL,
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index e76ffec..26a091d 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -2100,8 +2100,12 @@ function ( $column ) {
} else {
$is_unique = '0' === $info['NON_UNIQUE'];
+ // Prefix the original index name with the table name.
+ // This is to avoid conflicting index names in SQLite.
+ $index_name = $table_name . '__' . $info['INDEX_NAME'];
+
$query = sprintf( 'CREATE %sINDEX', $is_unique ? 'UNIQUE ' : '' );
- $query .= sprintf( ' "%s"', $info['INDEX_NAME'] );
+ $query .= sprintf( ' "%s"', $index_name );
$query .= sprintf( ' ON "%s" (', $table_name );
$query .= implode(
', ',
From b227db5d63f8fff3b8a120b7eb7f146a80883088 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 21 Jan 2025 15:25:25 +0100
Subject: [PATCH 053/124] Add support for multi-table delete statements
---
.../sqlite-ast/class-wp-sqlite-driver.php | 101 ++++++++++++++++--
1 file changed, 91 insertions(+), 10 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 26a091d..1543675 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -905,24 +905,23 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$this->execute_select_statement( $ast );
break;
case 'insertStatement':
+ $this->query_type = 'INSERT';
$this->execute_insert_statement( $ast );
break;
case 'updateStatement':
+ $this->query_type = 'UPDATE';
$this->execute_update_statement( $ast );
break;
case 'replaceStatement':
- case 'deleteStatement':
- if ( 'insertStatement' === $ast->rule_name ) {
- $this->query_type = 'INSERT';
- } elseif ( 'replaceStatement' === $ast->rule_name ) {
- $this->query_type = 'REPLACE';
- } elseif ( 'deleteStatement' === $ast->rule_name ) {
- $this->query_type = 'DELETE';
- }
- $query = $this->translate( $ast );
+ $this->query_type = 'REPLACE';
+ $query = $this->translate( $ast );
$this->execute_sqlite_query( $query );
$this->set_result_from_affected_rows();
break;
+ case 'deleteStatement':
+ $this->query_type = 'DELETE';
+ $this->execute_delete_statement( $ast );
+ break;
case 'createStatement':
$this->query_type = 'CREATE';
$subtree = $ast->get_child_node();
@@ -1132,6 +1131,88 @@ private function execute_update_statement( WP_Parser_Node $node ): void {
$this->set_result_from_affected_rows();
}
+ private function execute_delete_statement( WP_Parser_Node $node ): void {
+ /*
+ * Multi-table DELETE.
+ *
+ * MySQL supports multi-table DELETE statements that don't work in SQLite.
+ * These statements can have the following two flavours:
+ * 1. "DELETE t1, t2 FROM ... JOIN ... WHERE ..."
+ * 2. "DELETE FROM t1, t2 USING ... JOIN ... WHERE ..."
+ *
+ * We will rewrite such statements into a SELECT to fetch the ROWIDs of
+ * the rows to delete and then execute a DELETE statement for each table.
+ */
+ $alias_ref_list = $node->get_child_node( 'tableAliasRefList' );
+ if ( null !== $alias_ref_list ) {
+ // 1. Get table aliases targeted by the DELETE statement.
+ $table_aliases = array();
+ foreach ( $alias_ref_list->get_child_nodes() as $alias_ref ) {
+ $table_aliases[] = $this->unquote_sqlite_identifier(
+ $this->translate( $alias_ref )
+ );
+ }
+
+ // 2. Create an alias to table name map.
+ $alias_map = array();
+ $table_ref_list = $node->get_child_node( 'tableReferenceList' );
+ foreach ( $table_ref_list->get_descendant_nodes( 'singleTable' ) as $single_table ) {
+ $alias = $this->unquote_sqlite_identifier(
+ $this->translate( $single_table->get_child_node( 'tableAlias' ) )
+ );
+ $ref = $this->unquote_sqlite_identifier(
+ $this->translate( $single_table->get_child_node( 'tableRef' ) )
+ );
+
+ $alias_map[ $alias ] = $ref;
+ }
+
+ // 3. Compose the SELECT query to fetch ROWIDs to delete.
+ $where_clause = $node->get_child_node( 'whereClause' );
+ if ( null !== $where_clause ) {
+ $where = $this->translate( $where_clause->get_child_node( 'expr' ) );
+ }
+
+ $select_list = array();
+ foreach ( $table_aliases as $table ) {
+ $select_list[] = "\"$table\".rowid AS \"{$table}_rowid\"";
+ }
+
+ $ids = $this->execute_sqlite_query(
+ sprintf(
+ 'SELECT %s FROM %s %s',
+ implode( ', ', $select_list ),
+ $this->translate( $table_ref_list ),
+ isset( $where ) ? "WHERE $where" : ''
+ )
+ )->fetchAll( PDO::FETCH_ASSOC );
+
+ // 4. Execute DELETE statements for each table.
+ $rows = 0;
+ foreach ( $table_aliases as $table ) {
+ $this->execute_sqlite_query(
+ sprintf(
+ 'DELETE FROM "%s" AS %s WHERE rowid IN ( %s )',
+ $alias_map[ $table ],
+ $table,
+ implode( ', ', array_column( $ids, "{$table}_rowid" ) )
+ )
+ );
+ $this->set_result_from_affected_rows();
+ $rows += $this->affected_rows;
+ }
+
+ $this->set_result_from_affected_rows( $rows );
+ return;
+ }
+
+ // @TODO: Translate DELETE with JOIN to use a subquery.
+
+ $query = $this->translate( $node );
+ $this->execute_sqlite_query( $query );
+ $this->set_result_from_affected_rows();
+ }
+
private function execute_create_table_statement( WP_Parser_Node $node ): void {
$is_temporary = $node->get_child_node()->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL );
$element_list = $node->get_descendant_node( 'tableElementList' );
@@ -1784,7 +1865,7 @@ private function translate_like( WP_Parser_Node $node ): string {
* We'll probably need to overload the like() function:
* https://www.sqlite.org/lang_corefunc.html#like
*/
- return $this->translate_sequence( $node->get_children() );
+ return $this->translate_sequence( $node->get_children() ) . " ESCAPE '\\'";
}
private function translate_regexp_functions( WP_Parser_Node $node ): string {
From 9939dc3f8b28411c47a2e7ec9581a533c7bf12bb Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 21 Jan 2025 15:25:48 +0100
Subject: [PATCH 054/124] Remove no longer relevant TODOs
---
tests/WP_SQLite_Driver_Tests.php | 2 --
1 file changed, 2 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 3d63ef8..dcebdfe 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -285,7 +285,6 @@ public function testShowCreateTable1() {
'SHOW CREATE TABLE _tmp_table;'
);
$results = $this->engine->get_query_results();
- # TODO: Should we fix mismatch with original `option_value` text NOT NULL,` without default?
$this->assertEquals(
"CREATE TABLE `_tmp_table` (
`ID` bigint NOT NULL AUTO_INCREMENT,
@@ -314,7 +313,6 @@ public function testShowCreateTableQuoted() {
'SHOW CREATE TABLE `_tmp_table`;'
);
$results = $this->engine->get_query_results();
- # TODO: Should we fix mismatch with original `option_value` text NOT NULL,` without default?
$this->assertEquals(
"CREATE TABLE `_tmp_table` (
`ID` bigint NOT NULL AUTO_INCREMENT,
From 32a3a943985486da3d0d4aca05c80ce8c383bd02 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 21 Jan 2025 15:27:43 +0100
Subject: [PATCH 055/124] Remove no longer used data types cache
---
.../sqlite-ast/class-wp-sqlite-driver.php | 24 -------------------
1 file changed, 24 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 1543675..6192f85 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -196,15 +196,6 @@ class WP_SQLite_Driver {
'%y' => '%y',
);
- const DATA_TYPES_CACHE_TABLE = '_mysql_data_types_cache';
-
- const CREATE_DATA_TYPES_CACHE_TABLE = 'CREATE TABLE IF NOT EXISTS _mysql_data_types_cache (
- `table` TEXT NOT NULL,
- `column_or_index` TEXT NOT NULL,
- `mysql_type` TEXT NOT NULL,
- PRIMARY KEY(`table`, `column_or_index`)
- );';
-
/**
* @var WP_Parser_Grammar
*/
@@ -370,13 +361,6 @@ class WP_SQLite_Driver {
*/
private $pdo_fetch_mode;
- /**
- * Associative array with list of system (non-WordPress) tables.
- *
- * @var array [tablename => tablename]
- */
- private $sqlite_system_tables = array();
-
/**
* The last error message from SQLite.
*
@@ -446,14 +430,6 @@ public function __construct( string $db_name, ?PDO $pdo = null ) {
// MySQL data comes across stringified by default.
$pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
- $pdo->query( self::CREATE_DATA_TYPES_CACHE_TABLE );
-
- /*
- * A list of system tables lets us emulate information_schema
- * queries without returning extra tables.
- */
- $this->sqlite_system_tables ['sqlite_sequence'] = 'sqlite_sequence';
- $this->sqlite_system_tables [ self::DATA_TYPES_CACHE_TABLE ] = self::DATA_TYPES_CACHE_TABLE;
$this->pdo = $pdo;
From 3dfc04327caab130d75ad09550ce19af0c70808c Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 21 Jan 2025 15:38:19 +0100
Subject: [PATCH 056/124] Remove destructor
---
.../sqlite-ast/class-wp-sqlite-driver.php | 39 -------------------
1 file changed, 39 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 6192f85..46fe372 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -447,8 +447,6 @@ public function __construct( string $db_name, ?PDO $pdo = null ) {
// Fixes a warning in the site-health screen.
$this->client_info = SQLite3::version()['versionString'];
- register_shutdown_function( array( $this, '__destruct' ) );
-
// WordPress happens to use no foreign keys.
$statement = $this->pdo->query( 'PRAGMA foreign_keys' );
// phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
@@ -463,43 +461,6 @@ public function __construct( string $db_name, ?PDO $pdo = null ) {
}
}
- /**
- * Destructor
- *
- * If SQLITE_MEM_DEBUG constant is defined, append information about
- * memory usage into database/mem_debug.txt.
- *
- * This definition is changed since version 1.7.
- */
- public function __destruct() {
- if ( defined( 'SQLITE_MEM_DEBUG' ) && SQLITE_MEM_DEBUG ) {
- $max = ini_get( 'memory_limit' );
- if ( is_null( $max ) ) {
- $message = sprintf(
- '[%s] Memory_limit is not set in php.ini file.',
- gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] )
- );
- error_log( $message );
- return;
- }
- if ( stripos( $max, 'M' ) !== false ) {
- $max = (int) $max * MB_IN_BYTES;
- }
- $peak = memory_get_peak_usage( true );
- $used = round( (int) $peak / (int) $max * 100, 2 );
- if ( $used > 90 ) {
- $message = sprintf(
- "[%s] Memory peak usage warning: %s %% used. (max: %sM, now: %sM)\n",
- gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] ),
- $used,
- $max,
- $peak
- );
- error_log( $message );
- }
- }
- }
-
/**
* Get the PDO object.
*
From 7501c36550286bcf16978cece0c938a1a5fcc331 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 21 Jan 2025 15:50:37 +0100
Subject: [PATCH 057/124] Remove unused variable
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 1 -
1 file changed, 1 deletion(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 46fe372..930b1a0 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -2312,7 +2312,6 @@ private function unquote_sqlite_identifier( string $quoted_identifier ): string
* It is executed only once when the installation begins.
*/
private function prepare_directory() {
- global $wpdb;
$u = umask( 0000 );
if ( ! is_dir( FQDBDIR ) ) {
if ( ! @mkdir( FQDBDIR, 0704, true ) ) {
From 7d8742135433fec97d58ec657a1a686c21dcd659 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 10:43:38 +0100
Subject: [PATCH 058/124] Remove unused property
---
.../sqlite-ast/class-wp-sqlite-driver.php | 17 ++++-------------
1 file changed, 4 insertions(+), 13 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 930b1a0..306ecf6 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -361,13 +361,6 @@ class WP_SQLite_Driver {
*/
private $pdo_fetch_mode;
- /**
- * The last error message from SQLite.
- *
- * @var string
- */
- private $last_sqlite_error;
-
/**
* @var WP_SQLite_Information_Schema_Builder
*/
@@ -673,14 +666,12 @@ public function execute_sqlite_query( $sql, $params = array() ) {
if ( false === $stmt || null === $stmt ) {
$this->last_exec_returned = null;
$info = $this->pdo->errorInfo();
- $this->last_sqlite_error = $info[0] . ' ' . $info[2];
throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] );
}
- $returned = $stmt->execute( $params );
- $this->last_exec_returned = $returned;
- if ( ! $returned ) {
- $info = $stmt->errorInfo();
- $this->last_sqlite_error = $info[0] . ' ' . $info[2];
+
+ $this->last_exec_returned = $stmt->execute( $params );
+ if ( false === $this->last_exec_returned ) {
+ $info = $stmt->errorInfo();
throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] );
}
From bb49c902d6bebcf6521ac0b803e6933a3901a78a Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 12:30:26 +0100
Subject: [PATCH 059/124] Simplify setting foreign keys pragma
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 306ecf6..7ab854a 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -440,12 +440,7 @@ public function __construct( string $db_name, ?PDO $pdo = null ) {
// Fixes a warning in the site-health screen.
$this->client_info = SQLite3::version()['versionString'];
- // WordPress happens to use no foreign keys.
- $statement = $this->pdo->query( 'PRAGMA foreign_keys' );
- // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
- if ( $statement->fetchColumn( 0 ) == '0' ) {
- $this->pdo->query( 'PRAGMA foreign_keys = ON' );
- }
+ $this->pdo->query( 'PRAGMA foreign_keys = ON' );
$this->pdo->query( 'PRAGMA encoding="UTF-8";' );
$valid_journal_modes = array( 'DELETE', 'TRUNCATE', 'PERSIST', 'MEMORY', 'WAL', 'OFF' );
From 09edeaa4c42838939d1ba086c785c4ca6609d34f Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 12:59:57 +0100
Subject: [PATCH 060/124] Use AST to detect transactional statements
---
.../sqlite-ast/class-wp-sqlite-driver.php | 51 ++++++++++++++-----
1 file changed, 39 insertions(+), 12 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 7ab854a..c31ea82 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -536,18 +536,6 @@ public function query( $statement, $mode = PDO::FETCH_OBJ, ...$fetch_mode_args )
}
$this->pdo_fetch_mode = $mode;
$this->mysql_query = $statement;
- if (
- preg_match( '/^\s*START TRANSACTION/i', $statement )
- || preg_match( '/^\s*BEGIN/i', $statement )
- ) {
- return $this->begin_transaction();
- }
- if ( preg_match( '/^\s*COMMIT/i', $statement ) ) {
- return $this->commit();
- }
- if ( preg_match( '/^\s*ROLLBACK/i', $statement ) ) {
- return $this->rollback();
- }
try {
// Parse the MySQL query.
@@ -561,6 +549,45 @@ public function query( $statement, $mode = PDO::FETCH_OBJ, ...$fetch_mode_args )
throw new Exception( 'Failed to parse the MySQL query.' );
}
+ // Handle transactional statements.
+ $child = $ast->get_child();
+ if ( $child instanceof WP_Parser_Node && 'beginWork' === $child->rule_name ) {
+ return $this->begin_transaction();
+ }
+
+ if ( $child instanceof WP_Parser_Node && 'simpleStatement' === $child->rule_name ) {
+ $subchild = $child->get_child_node( 'transactionOrLockingStatement' );
+ if ( null !== $subchild ) {
+ $tokens = $subchild->get_descendant_tokens();
+ $token1 = $tokens[0];
+ $token2 = $tokens[1] ?? null;
+ if (
+ WP_MySQL_Lexer::START_SYMBOL === $token1->id
+ && WP_MySQL_Lexer::TRANSACTION_SYMBOL === $token2->id
+ ) {
+ return $this->begin_transaction();
+ }
+
+ if (
+ WP_MySQL_Lexer::BEGIN_SYMBOL === $token1->id
+ ) {
+ return $this->begin_transaction();
+ }
+
+ if (
+ WP_MySQL_Lexer::COMMIT_SYMBOL === $token1->id
+ ) {
+ return $this->commit();
+ }
+
+ if (
+ WP_MySQL_Lexer::ROLLBACK_SYMBOL === $token1->id
+ ) {
+ return $this->rollback();
+ }
+ }
+ }
+
// Perform all the queries in a nested transaction.
$this->begin_transaction();
From 86f3b7fb8c8ce35a789945043aa075a22de16cbc Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 13:02:44 +0100
Subject: [PATCH 061/124] Convert IFs to switch statement
---
.../sqlite-ast/class-wp-sqlite-driver.php | 48 +++++++++----------
1 file changed, 24 insertions(+), 24 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index c31ea82..db64cad 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1210,37 +1210,37 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void {
foreach ( $node->get_descendant_nodes( 'alterListItem' ) as $action ) {
$first_token = $action->get_child_token();
- if ( WP_MySQL_Lexer::DROP_SYMBOL === $first_token->id ) {
- $name = $this->translate( $action->get_child_node( 'columnInternalRef' ) );
- if ( null !== $name ) {
- $name = $this->unquote_sqlite_identifier( $name );
- unset( $column_map[ $name ] );
- }
- }
-
- if ( WP_MySQL_Lexer::CHANGE_SYMBOL === $first_token->id ) {
- $old_name = $this->unquote_sqlite_identifier(
- $this->translate( $action->get_child_node( 'columnInternalRef' ) )
- );
- $new_name = $this->unquote_sqlite_identifier(
- $this->translate( $action->get_child_node( 'identifier' ) )
- );
-
- $column_map[ $old_name ] = $new_name;
- }
-
- if ( WP_MySQL_Lexer::RENAME_SYMBOL === $first_token->id ) {
- $column_ref = $action->get_child_node( 'columnInternalRef' );
- if ( null !== $column_ref ) {
+ switch ( $first_token->id ) {
+ case WP_MySQL_Lexer::DROP_SYMBOL:
+ $name = $this->translate( $action->get_child_node( 'columnInternalRef' ) );
+ if ( null !== $name ) {
+ $name = $this->unquote_sqlite_identifier( $name );
+ unset( $column_map[ $name ] );
+ }
+ break;
+ case WP_MySQL_Lexer::CHANGE_SYMBOL:
$old_name = $this->unquote_sqlite_identifier(
- $this->translate( $column_ref )
+ $this->translate( $action->get_child_node( 'columnInternalRef' ) )
);
$new_name = $this->unquote_sqlite_identifier(
$this->translate( $action->get_child_node( 'identifier' ) )
);
$column_map[ $old_name ] = $new_name;
- }
+ break;
+ case WP_MySQL_Lexer::RENAME_SYMBOL:
+ $column_ref = $action->get_child_node( 'columnInternalRef' );
+ if ( null !== $column_ref ) {
+ $old_name = $this->unquote_sqlite_identifier(
+ $this->translate( $column_ref )
+ );
+ $new_name = $this->unquote_sqlite_identifier(
+ $this->translate( $action->get_child_node( 'identifier' ) )
+ );
+
+ $column_map[ $old_name ] = $new_name;
+ }
+ break;
}
}
From 49c9794fa9b49c3ec586cd921d545b6e57584dc4 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 13:06:35 +0100
Subject: [PATCH 062/124] Remove outdated comment, add TODO
---
.../sqlite-ast/class-wp-sqlite-driver.php | 23 ++-----------------
1 file changed, 2 insertions(+), 21 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index db64cad..243144e 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1303,28 +1303,9 @@ function ( $column ) {
$this->results = 1;
$this->return_value = $this->results;
- return;
- /*
- * SQLite supports only a small subset of MySQL ALTER TABLE statement.
- * We need to handle some differences and emulate some operations:
- *
- * 1. Multiple operations in a single ALTER TABLE statement.
- *
- * SQLite doesn't support multiple operations in a single ALTER TABLE
- * statement. We need to execute each operation as a separate query.
- *
- * 2. ADD COLUMN in SQLite doesn't support some valid MySQL constructs:
- *
- * - Adding a column with PRIMARY KEY or UNIQUE constraint.
- * - Adding a column with AUTOINCREMENT.
- * - Adding a column with CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP,
- * or an expression in parentheses as a default value.
- * - Adding a NOT NULL column without a default value when the table is
- * not empty. In MySQL, this depends on the data type and SQL mode.
- *
- * @TODO: Address these nuances.
- */
+ // @TODO: Consider using a "fast path" for ALTER TABLE statements that
+ // consist only of operations that SQLite's ALTER TABLE supports.
}
private function execute_drop_table_statement( WP_Parser_Node $node ): void {
From b0a4520ce3599d507aeae5d1f49449e9862b92ef Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 13:13:30 +0100
Subject: [PATCH 063/124] Improve comment
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 243144e..98239f1 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1057,17 +1057,20 @@ private function execute_update_statement( WP_Parser_Node $node ): void {
$parts = array();
foreach ( $node->get_children() as $child ) {
if ( $child instanceof WP_MySQL_Token && WP_MySQL_Lexer::IGNORE_SYMBOL === $child->id ) {
+ // Translate "UPDATE IGNORE" to "UPDATE OR IGNORE".
$parts[] = 'OR IGNORE';
} else {
$parts[] = $this->translate( $child );
}
+ // When using a subquery, skip WHERE, ORDER BY, and LIMIT.
if (
null !== $where_subquery
&& $child instanceof WP_Parser_Node
&& 'updateList' === $child->rule_name
) {
- // Skip WHERE, ORDER BY, and LIMIT.
+ // We can stop here, as the update statement grammar is:
+ // ... updateList whereClause? orderClause? simpleLimitClause?
break;
}
}
From e2453b70106a368a002e6e0026e9c02cca9ded66 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 13:21:51 +0100
Subject: [PATCH 064/124] Use get_child_node() calls to make translation
unambiguous
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 98239f1..7e5348a 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1046,10 +1046,10 @@ private function execute_update_statement( WP_Parser_Node $node ): void {
if ( $has_order || $has_limit ) {
$where_subquery = 'SELECT rowid FROM ' . $this->translate_sequence(
array(
- $node->get_descendant_node( 'tableReference' ),
- $node->get_descendant_node( 'whereClause' ),
- $node->get_descendant_node( 'orderClause' ),
- $node->get_descendant_node( 'simpleLimitClause' ),
+ $node->get_child_node( 'tableReferenceList' ),
+ $node->get_child_node( 'whereClause' ),
+ $node->get_child_node( 'orderClause' ),
+ $node->get_child_node( 'simpleLimitClause' ),
)
);
}
From d556c0926a782296e341996e271f37ee38ace006 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 13:27:38 +0100
Subject: [PATCH 065/124] Add a TODO comment for AST-querying API
---
wp-includes/parser/class-wp-parser-node.php | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/wp-includes/parser/class-wp-parser-node.php b/wp-includes/parser/class-wp-parser-node.php
index 89f8666..81ea137 100644
--- a/wp-includes/parser/class-wp-parser-node.php
+++ b/wp-includes/parser/class-wp-parser-node.php
@@ -262,4 +262,9 @@ public function get_descendant_tokens( ?int $token_id = null ): array {
}
return $all_descendants;
}
+
+ /*
+ * @TODO: Let's implement a more powerful AST-querying API.
+ * See: https://github.com/WordPress/sqlite-database-integration/pull/164#discussion_r1855230501
+ */
}
From 9c1feea383c86ee94e0a7279e4f4a1b6447721b3 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 14:01:54 +0100
Subject: [PATCH 066/124] Handle foreign key checks in ALTER TABLE
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 7e5348a..83d5c19 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1255,7 +1255,8 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void {
*/
// 1. If foreign key constraints are enabled, disable them.
- // @TODO
+ $pragma_foreign_keys = $this->execute_sqlite_query( 'PRAGMA foreign_keys' )->fetchColumn();
+ $this->execute_sqlite_query( 'PRAGMA foreign_keys = OFF' );
// 2. Create a new table with the new schema.
$tmp_table_name = "_tmp__{$table_name}_" . uniqid();
@@ -1302,6 +1303,12 @@ function ( $column ) {
$this->execute_sqlite_query( $query );
}
+ // 7. If foreign key constraints were enabled, verify and enable them.
+ if ( '1' === $pragma_foreign_keys ) {
+ $this->execute_sqlite_query( 'PRAGMA foreign_key_check' );
+ $this->execute_sqlite_query( 'PRAGMA foreign_keys = ON' );
+ }
+
// @TODO: Triggers and views.
$this->results = 1;
From 57287db343b4195766b21ecf0ef3d0112e0336be Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 14:27:03 +0100
Subject: [PATCH 067/124] Improve query method docs and naming
---
.../sqlite-ast/class-wp-sqlite-driver.php | 46 ++++++-------------
1 file changed, 14 insertions(+), 32 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 83d5c19..67e87b8 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -473,37 +473,19 @@ public function get_affected_rows() {
}
/**
- * Method to execute query().
+ * Translate and execute a MySQL query in SQLite.
*
- * Divide the query types into seven different ones. That is to say:
+ * A single MySQL query can be translated into zero or more SQLite queries.
*
- * 1. SELECT SQL_CALC_FOUND_ROWS
- * 2. INSERT
- * 3. CREATE TABLE(INDEX)
- * 4. ALTER TABLE
- * 5. SHOW VARIABLES
- * 6. DROP INDEX
- * 7. THE OTHERS
- *
- * #1 is just a tricky play. See the private function handle_sql_count() in query.class.php.
- * From #2 through #5 call different functions respectively.
- * #6 call the ALTER TABLE query.
- * #7 is a normal process: sequentially call prepare_query() and execute_query().
- *
- * #1 process has been changed since version 1.5.1.
- *
- * @param string $statement Full SQL statement string.
- * @param int $mode Not used.
- * @param array ...$fetch_mode_args Not used.
- *
- * @see PDO::query()
+ * @param string $query Full SQL statement string.
+ * @param int $fetch_mode PDO fetch mode. Default is PDO::FETCH_OBJ.
+ * @param array ...$fetch_mode_args Additional fetch mode arguments.
*
+ * @return mixed Return value, depending on the query type.
* @throws Exception If the query could not run.
* @throws PDOException If the translated query could not run.
- *
- * @return mixed according to the query type
*/
- public function query( $statement, $mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) { // phpcs:ignore WordPress.DB.RestrictedClasses
+ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) { // phpcs:ignore WordPress.DB.RestrictedClasses
$this->flush();
if ( function_exists( 'apply_filters' ) ) {
/**
@@ -522,24 +504,24 @@ public function query( $statement, $mode = PDO::FETCH_OBJ, ...$fetch_mode_args )
*
* @param null|array $result Default null to continue with the query.
* @param object $translator The translator object. You can call $translator->execute_sqlite_query().
- * @param string $statement The statement passed.
- * @param int $mode Fetch mode: PDO::FETCH_OBJ, PDO::FETCH_CLASS, etc.
+ * @param string $query The statement passed.
+ * @param int $fetch_mode Fetch mode: PDO::FETCH_OBJ, PDO::FETCH_CLASS, etc.
* @param array $fetch_mode_args Variable arguments passed to query.
*
* @returns null|array Null to proceed, or an array containing a resultset.
* @since 2.1.0
*/
- $pre = apply_filters( 'pre_query_sqlite_db', null, $this, $statement, $mode, $fetch_mode_args );
+ $pre = apply_filters( 'pre_query_sqlite_db', null, $this, $query, $fetch_mode, $fetch_mode_args );
if ( null !== $pre ) {
return $pre;
}
}
- $this->pdo_fetch_mode = $mode;
- $this->mysql_query = $statement;
+ $this->pdo_fetch_mode = $fetch_mode;
+ $this->mysql_query = $query;
try {
// Parse the MySQL query.
- $lexer = new WP_MySQL_Lexer( $statement );
+ $lexer = new WP_MySQL_Lexer( $query );
$tokens = $lexer->remaining_tokens();
$parser = new WP_MySQL_Parser( self::$grammar, $tokens );
@@ -549,7 +531,7 @@ public function query( $statement, $mode = PDO::FETCH_OBJ, ...$fetch_mode_args )
throw new Exception( 'Failed to parse the MySQL query.' );
}
- // Handle transactional statements.
+ // Handle transaction commands.
$child = $ast->get_child();
if ( $child instanceof WP_Parser_Node && 'beginWork' === $child->rule_name ) {
return $this->begin_transaction();
From fa903bcd654daf4c7c6854535eaa83cd515af756 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 14:43:52 +0100
Subject: [PATCH 068/124] Fix identifier escaping
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 7 +++++--
.../class-wp-sqlite-information-schema-builder.php | 1 -
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 67e87b8..fe08a40 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1702,9 +1702,12 @@ private function translate_token( WP_MySQL_Token $token ) {
case WP_MySQL_Lexer::EOF:
return null;
case WP_MySQL_Lexer::IDENTIFIER:
+ return '"' . $token->value . '"';
case WP_MySQL_Lexer::BACK_TICK_QUOTED_ID:
- // @TODO: Properly unquote (MySQL) and escape (SQLite).
- return '"' . trim( $token->value, '`"' ) . '"';
+ $value = substr( $token->value, 1, -1 ); // remove backticks
+ $value = str_replace( '``', '`', $value ); // unescape from MySQL
+ $value = str_replace( '"', '""', $value ); // escape for SQLite
+ return '"' . $value . '"';
case WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL:
return 'AUTOINCREMENT';
case WP_MySQL_Lexer::BINARY_SYMBOL:
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index bd45f7a..e89bbeb 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -1465,7 +1465,6 @@ private function get_value( WP_Parser_Node $node ): string {
$value = $this->get_value( $child );
} elseif ( WP_MySQL_Lexer::BACK_TICK_QUOTED_ID === $child->id ) {
$value = substr( $child->value, 1, -1 );
- $value = str_replace( '\`', '`', $value );
$value = str_replace( '``', '`', $value );
} elseif ( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT === $child->id ) {
$value = $child->value;
From 38d349166ae2482e9ed47e949d6f342d1f886fba Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 14:44:22 +0100
Subject: [PATCH 069/124] Throw exceptions for unsupported throw statements
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index fe08a40..4ac56d7 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1392,7 +1392,13 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
$this->execute_show_tables_statement( $node );
break;
default:
- // @TODO
+ throw new Exception(
+ sprintf(
+ 'Unsupported statement type: "%s" > "%s"',
+ $node->rule_name,
+ $keyword1->value
+ )
+ );
}
}
From 76285eaff860bbb8e5523772d9f05ad9ded9e0a8 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 14:57:37 +0100
Subject: [PATCH 070/124] Unify identifier quoting
---
.../sqlite-ast/class-wp-sqlite-driver.php | 90 ++++++++++---------
1 file changed, 46 insertions(+), 44 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 4ac56d7..31b777d 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1241,44 +1241,42 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void {
$this->execute_sqlite_query( 'PRAGMA foreign_keys = OFF' );
// 2. Create a new table with the new schema.
- $tmp_table_name = "_tmp__{$table_name}_" . uniqid();
- $queries = $this->get_sqlite_create_table_statement( $table_name, $tmp_table_name );
- $create_table_query = $queries[0];
- $constraint_queries = array_slice( $queries, 1 );
+ $tmp_table_name = "_tmp__{$table_name}_" . uniqid();
+ $quoted_table_name = $this->quote_sqlite_identifier( $table_name );
+ $quoted_tmp_table_name = $this->quote_sqlite_identifier( $tmp_table_name );
+ $queries = $this->get_sqlite_create_table_statement( $table_name, $tmp_table_name );
+ $create_table_query = $queries[0];
+ $constraint_queries = array_slice( $queries, 1 );
$this->execute_sqlite_query( $create_table_query );
// 3. Copy data from the original table to the new table.
$this->execute_sqlite_query(
sprintf(
- 'INSERT INTO "%s" (%s) SELECT %s FROM "%s"',
- $tmp_table_name,
+ 'INSERT INTO %s (%s) SELECT %s FROM %s',
+ $quoted_tmp_table_name,
implode(
', ',
- array_map(
- function ( $column ) {
- return '"' . $column . '"';
- },
- $column_map
- )
+ array_map( array( $this, 'quote_sqlite_identifier' ), $column_map )
),
implode(
', ',
- array_map(
- function ( $column ) {
- return '"' . $column . '"';
- },
- array_keys( $column_map )
- )
+ array_map( array( $this, 'quote_sqlite_identifier' ), array_keys( $column_map ) )
),
- $table_name
+ $quoted_table_name
)
);
// 4. Drop the original table.
- $this->execute_sqlite_query( sprintf( 'DROP TABLE "%s"', $table_name ) );
+ $this->execute_sqlite_query( sprintf( 'DROP TABLE %s', $quoted_table_name ) );
// 5. Rename the new table to the original table name.
- $this->execute_sqlite_query( sprintf( 'ALTER TABLE "%s" RENAME TO "%s"', $tmp_table_name, $table_name ) );
+ $this->execute_sqlite_query(
+ sprintf(
+ 'ALTER TABLE %s RENAME TO %s',
+ $quoted_tmp_table_name,
+ $quoted_table_name
+ )
+ );
// 6. Reconstruct indexes, triggers, and views.
foreach ( $constraint_queries as $query ) {
@@ -2031,7 +2029,7 @@ private function get_sqlite_create_table_statement( string $table_name, ?string
$has_autoincrement = false;
foreach ( $column_info as $column ) {
$query = ' ';
- $query .= sprintf( '"%s"', str_replace( '"', '""', $column['COLUMN_NAME'] ) );
+ $query .= $this->quote_sqlite_identifier( $column['COLUMN_NAME'] );
$type = self::DATA_TYPE_STRING_MAP[ $column['DATA_TYPE'] ];
@@ -2103,7 +2101,7 @@ private function get_sqlite_create_table_statement( string $table_name, ?string
', ',
array_map(
function ( $column ) {
- return sprintf( '"%s"', str_replace( '"', '""', $column['COLUMN_NAME'] ) );
+ return $this->quote_sqlite_identifier( $column['COLUMN_NAME'] );
},
$constraint
)
@@ -2124,7 +2122,7 @@ function ( $column ) {
', ',
array_map(
function ( $column ) {
- return sprintf( '"%s"', str_replace( '"', '""', $column['COLUMN_NAME'] ) );
+ return $this->quote_sqlite_identifier( $column['COLUMN_NAME'] );
},
$constraint
)
@@ -2136,7 +2134,10 @@ function ( $column ) {
}
// 6. Compose the CREATE TABLE statement.
- $create_table_query = sprintf( 'CREATE TABLE "%s" (%s', str_replace( '"', '""', $new_table_name ?? $table_name ), "\n" );
+ $create_table_query = sprintf(
+ "CREATE TABLE %s (\n",
+ $this->quote_sqlite_identifier( $new_table_name ?? $table_name )
+ );
$create_table_query .= implode( ",\n", $rows );
$create_table_query .= "\n)";
return array_merge( array( $create_table_query ), $create_index_queries, $on_update_queries );
@@ -2181,10 +2182,8 @@ private function get_mysql_create_table_statement( string $table_name ): ?string
// 4. Generate CREATE TABLE statement columns.
$rows = array();
foreach ( $column_info as $column ) {
- $sql = ' ';
- // @TODO: Proper identifier escaping.
- $sql .= sprintf( '`%s`', str_replace( '`', '``', $column['COLUMN_NAME'] ) );
-
+ $sql = ' ';
+ $sql .= $this->quote_mysql_identifier( $column['COLUMN_NAME'] );
$sql .= ' ' . $column['COLUMN_TYPE'];
if ( 'NO' === $column['IS_NULLABLE'] ) {
$sql .= ' NOT NULL';
@@ -2210,8 +2209,7 @@ private function get_mysql_create_table_statement( string $table_name ): ?string
', ',
array_map(
function ( $column ) {
- // @TODO: Proper identifier escaping.
- return sprintf( '`%s`', str_replace( '`', '``', $column['COLUMN_NAME'] ) );
+ return $this->quote_mysql_identifier( $column['COLUMN_NAME'] );
},
$constraint
)
@@ -2221,16 +2219,14 @@ function ( $column ) {
} else {
$is_unique = '0' === $info['NON_UNIQUE'];
- $sql = sprintf( ' %sKEY', $is_unique ? 'UNIQUE ' : '' );
- // @TODO: Proper identifier escaping.
- $sql .= sprintf( ' `%s`', str_replace( '`', '``', $info['INDEX_NAME'] ) );
+ $sql = sprintf( ' %sKEY ', $is_unique ? 'UNIQUE ' : '' );
+ $sql .= $this->quote_mysql_identifier( $info['INDEX_NAME'] );
$sql .= ' (';
$sql .= implode(
', ',
array_map(
function ( $column ) {
- // @TODO: Proper identifier escaping.
- return sprintf( '`%s`', str_replace( '`', '``', $column['COLUMN_NAME'] ) );
+ return $this->quote_mysql_identifier( $column['COLUMN_NAME'] );
},
$constraint
)
@@ -2242,17 +2238,15 @@ function ( $column ) {
}
// 5. Compose the CREATE TABLE statement.
- // @TODO: Proper identifier escaping.
- $sql = sprintf( 'CREATE TABLE `%s` (%s', str_replace( '`', '``', $table_name ), "\n" );
+ $collation = $table_info['TABLE_COLLATION'];
+ $charset = substr( $collation, 0, strpos( $collation, '_' ) );
+
+ $sql = sprintf( "CREATE TABLE %s (\n", $this->quote_mysql_identifier( $table_name ) );
$sql .= implode( ",\n", $rows );
$sql .= "\n)";
-
$sql .= sprintf( ' ENGINE=%s', $table_info['ENGINE'] );
-
- $collation = $table_info['TABLE_COLLATION'];
- $charset = substr( $collation, 0, strpos( $collation, '_' ) );
- $sql .= sprintf( ' DEFAULT CHARSET=%s', $charset );
- $sql .= sprintf( ' COLLATE=%s', $collation );
+ $sql .= sprintf( ' DEFAULT CHARSET=%s', $charset );
+ $sql .= sprintf( ' COLLATE=%s', $collation );
return $sql;
}
@@ -2301,6 +2295,14 @@ private function unquote_sqlite_identifier( string $quoted_identifier ): string
return str_replace( '""', '"', $unquoted );
}
+ private function quote_sqlite_identifier( string $unquoted_identifier ): string {
+ return '"' . str_replace( '"', '""', $unquoted_identifier ) . '"';
+ }
+
+ private function quote_mysql_identifier( string $unquoted_identifier ): string {
+ return '`' . str_replace( '`', '``', $unquoted_identifier ) . '`';
+ }
+
/**
* This method makes database directory and .htaccess file.
*
From 3530023a26f82cb06e696bddfa348ead958c8f73 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 16:50:31 +0100
Subject: [PATCH 071/124] Improve API of user defined functions
---
...P_SQLite_PDO_User_Defined_Functions_Tests.php | 2 +-
.../sqlite-ast/class-wp-sqlite-driver.php | 2 +-
...lass-wp-sqlite-pdo-user-defined-functions.php | 16 +++++++---------
.../sqlite/class-wp-sqlite-translator.php | 2 +-
4 files changed, 10 insertions(+), 12 deletions(-)
diff --git a/tests/WP_SQLite_PDO_User_Defined_Functions_Tests.php b/tests/WP_SQLite_PDO_User_Defined_Functions_Tests.php
index 9824148..4ba6ee1 100644
--- a/tests/WP_SQLite_PDO_User_Defined_Functions_Tests.php
+++ b/tests/WP_SQLite_PDO_User_Defined_Functions_Tests.php
@@ -10,7 +10,7 @@ class WP_SQLite_PDO_User_Defined_Functions_Tests extends TestCase {
*/
public function testFieldFunction( $expected, $args ) {
$pdo = new PDO( 'sqlite::memory:' );
- $fns = new WP_SQLite_PDO_User_Defined_Functions( $pdo );
+ $fns = WP_SQLite_PDO_User_Defined_Functions::register_for( $pdo );
$this->assertEquals(
$expected,
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 31b777d..ac0744f 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -419,7 +419,7 @@ public function __construct( string $db_name, ?PDO $pdo = null ) {
}
}
- new WP_SQLite_PDO_User_Defined_Functions( $pdo );
+ WP_SQLite_PDO_User_Defined_Functions::register_for( $pdo );
// MySQL data comes across stringified by default.
$pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
diff --git a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php
index 010e5a9..bbf1828 100644
--- a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php
+++ b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php
@@ -23,19 +23,17 @@
class WP_SQLite_PDO_User_Defined_Functions {
/**
- * The class constructor
- *
- * Initializes the use defined functions to PDO object with PDO::sqliteCreateFunction().
+ * Registers the user defined functions for SQLite to a PDO instance.
+ * The functions are registered using PDO::sqliteCreateFunction().
*
* @param PDO $pdo The PDO object.
*/
- public function __construct( $pdo ) {
- if ( ! $pdo ) {
- wp_die( 'Database is not initialized.', 'Database Error' );
- }
- foreach ( $this->functions as $f => $t ) {
- $pdo->sqliteCreateFunction( $f, array( $this, $t ) );
+ public static function register_for( PDO $pdo ): self {
+ $instance = new self();
+ foreach ( $instance->functions as $f => $t ) {
+ $pdo->sqliteCreateFunction( $f, array( $instance, $t ) );
}
+ return $instance;
}
/**
diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php
index 44996cd..6892de0 100644
--- a/wp-includes/sqlite/class-wp-sqlite-translator.php
+++ b/wp-includes/sqlite/class-wp-sqlite-translator.php
@@ -394,7 +394,7 @@ public function __construct( $pdo = null ) {
}
}
- new WP_SQLite_PDO_User_Defined_Functions( $pdo );
+ WP_SQLite_PDO_User_Defined_Functions::register_for( $pdo );
// MySQL data comes across stringified by default.
$pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
From 677bca44b3ce229932d8ac52e1f49bf54edc100d Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 17:02:18 +0100
Subject: [PATCH 072/124] Add a TODO for SQLite client info
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 3 +++
1 file changed, 3 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index ac0744f..7deb47c 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -438,6 +438,9 @@ public function __construct( string $db_name, ?PDO $pdo = null ) {
}
// Fixes a warning in the site-health screen.
+ // @TODO: It is not clear how this fixes a warning or if it serves a
+ // different purpose. We should also make this use PDO and
+ // consider providing that information lazily via a getter.
$this->client_info = SQLite3::version()['versionString'];
$this->pdo->query( 'PRAGMA foreign_keys = ON' );
From 53eb81e862cc460ff64e4ccac2effc4c57a72e6d Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 23 Jan 2025 17:11:35 +0100
Subject: [PATCH 073/124] Use a unified prefix for internal objects
---
tests/WP_SQLite_Driver_Tests.php | 8 ++++----
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 11 +++++++++--
2 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index dcebdfe..ea3c805 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -1249,17 +1249,17 @@ public function testColumnWithOnUpdate() {
array(
(object) array(
'type' => 'trigger',
- 'name' => '___tmp_table_created_at_on_update__',
+ 'name' => '_wp_sqlite__tmp_table_created_at_on_update',
'tbl_name' => '_tmp_table',
'rootpage' => '0',
- 'sql' => "CREATE TRIGGER \"___tmp_table_created_at_on_update__\"\n\t\t\tAFTER UPDATE ON \"_tmp_table\"\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE \"_tmp_table\" SET \"created_at\" = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;\n\t\t\tEND",
+ 'sql' => "CREATE TRIGGER \"_wp_sqlite__tmp_table_created_at_on_update\"\n\t\t\tAFTER UPDATE ON \"_tmp_table\"\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE \"_tmp_table\" SET \"created_at\" = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;\n\t\t\tEND",
),
(object) array(
'type' => 'trigger',
- 'name' => '___tmp_table_updated_at_on_update__',
+ 'name' => '_wp_sqlite__tmp_table_updated_at_on_update',
'tbl_name' => '_tmp_table',
'rootpage' => '0',
- 'sql' => "CREATE TRIGGER \"___tmp_table_updated_at_on_update__\"\n\t\t\tAFTER UPDATE ON \"_tmp_table\"\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE \"_tmp_table\" SET \"updated_at\" = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;\n\t\t\tEND",
+ 'sql' => "CREATE TRIGGER \"_wp_sqlite__tmp_table_updated_at_on_update\"\n\t\t\tAFTER UPDATE ON \"_tmp_table\"\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE \"_tmp_table\" SET \"updated_at\" = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;\n\t\t\tEND",
),
),
$results
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 7deb47c..084d7d5 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -21,6 +21,13 @@ class WP_SQLite_Driver {
const SQLITE_BUSY = 5;
const SQLITE_LOCKED = 6;
+ /**
+ * An identifier prefix for internal objects.
+ *
+ * @TODO: Do not allow accessing objects with this prefix.
+ */
+ const RESERVED_PREFIX = '_wp_sqlite_';
+
/**
* A map of MySQL tokens to SQLite data types.
*
@@ -1244,7 +1251,7 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void {
$this->execute_sqlite_query( 'PRAGMA foreign_keys = OFF' );
// 2. Create a new table with the new schema.
- $tmp_table_name = "_tmp__{$table_name}_" . uniqid();
+ $tmp_table_name = self::RESERVED_PREFIX . "tmp_{$table_name}_" . uniqid();
$quoted_table_name = $this->quote_sqlite_identifier( $table_name );
$quoted_tmp_table_name = $this->quote_sqlite_identifier( $tmp_table_name );
$queries = $this->get_sqlite_create_table_statement( $table_name, $tmp_table_name );
@@ -2277,7 +2284,7 @@ private function get_column_on_update_trigger_query( string $table, string $colu
// The trigger wouldn't work for virtual and "WITHOUT ROWID" tables,
// but currently that can't happen as we're not creating such tables.
// See: https://www.sqlite.org/rowidtable.html
- $trigger_name = "__{$table}_{$column}_on_update__";
+ $trigger_name = self::RESERVED_PREFIX . "{$table}_{$column}_on_update";
return "
CREATE TRIGGER \"$trigger_name\"
AFTER UPDATE ON \"$table\"
From fd6c2fa7c806b4b8731a9e976d4632bf51d8fd72 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 24 Jan 2025 09:25:45 +0100
Subject: [PATCH 074/124] Fetch column info only for needed columns when adding
a constraint
---
...s-wp-sqlite-information-schema-builder.php | 46 ++++++++++++-------
1 file changed, 29 insertions(+), 17 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index e89bbeb..fdc81d2 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -668,21 +668,6 @@ private function record_add_constraint( string $table_name, WP_Parser_Node $node
throw new \Exception( 'FOREIGN KEY and CHECK constraints are not supported yet.' );
}
- // Fetch column info.
- $column_info = $this->query(
- '
- SELECT column_name, data_type, is_nullable, character_maximum_length
- FROM _mysql_information_schema_columns
- WHERE table_name = ?
- ',
- array( $table_name )
- )->fetchAll( PDO::FETCH_ASSOC );
-
- $column_info_map = array_combine(
- array_column( $column_info, 'COLUMN_NAME' ),
- $column_info
- );
-
// Get key parts.
$key_list = $node->get_child_node( 'keyListVariants' )->get_child();
if ( 'keyListWithExpression' === $key_list->rule_name ) {
@@ -694,6 +679,33 @@ private function record_add_constraint( string $table_name, WP_Parser_Node $node
$key_parts = $key_list->get_descendant_nodes( 'keyPart' );
}
+ // Get index column names.
+ $key_part_column_names = array();
+ foreach ( $key_parts as $key_part ) {
+ $key_part_column_names[] = $this->get_index_column_name( $key_part );
+ }
+
+ // Fetch column info.
+ $column_names = array_filter( $key_part_column_names );
+ if ( count( $column_names ) > 0 ) {
+ $column_info = $this->query(
+ '
+ SELECT column_name, data_type, is_nullable, character_maximum_length
+ FROM _mysql_information_schema_columns
+ WHERE table_name = ?
+ AND column_name IN (' . implode( ',', array_fill( 0, count( $column_names ), '?' ) ) . ')
+ ',
+ array_merge( array( $table_name ), $column_names )
+ )->fetchAll( PDO::FETCH_ASSOC );
+ } else {
+ $column_info = array();
+ }
+
+ $column_info_map = array_combine(
+ array_column( $column_info, 'COLUMN_NAME' ),
+ $column_info
+ );
+
// Get first index column data type (needed for index type).
$first_column_name = $this->get_index_column_name( $key_parts[0] );
$first_column_type = $column_info_map[ $first_column_name ]['DATA_TYPE'] ?? null;
@@ -704,8 +716,8 @@ private function record_add_constraint( string $table_name, WP_Parser_Node $node
$index_type = $this->get_index_type( $node, $keyword, $has_spatial_column );
$seq_in_index = 1;
- foreach ( $key_parts as $key_part ) {
- $column_name = $this->get_index_column_name( $key_part );
+ foreach ( $key_parts as $i => $key_part ) {
+ $column_name = $key_part_column_names[ $i ];
$collation = $this->get_index_column_collation( $key_part, $index_type );
if (
'PRIMARY' === $index_name
From 5b95d963450387ff2a4446c238276f81ab13878a Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 24 Jan 2025 10:48:59 +0100
Subject: [PATCH 075/124] Preserve ROWIDs in ALTER TABLE statements
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 084d7d5..fc67ad1 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1200,6 +1200,11 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void {
array( $this->db_name, $table_name )
)->fetchAll( PDO::FETCH_COLUMN );
+ // Preserve ROWIDs.
+ // This also addresses a special case when all original columns are dropped
+ // and there is nothing to copy. We'll always have at least the ROWID column.
+ array_unshift( $column_names, 'rowid' );
+
// Track column renames and removals.
$column_map = array_combine( $column_names, $column_names );
foreach ( $node->get_descendant_nodes( 'alterListItem' ) as $action ) {
From e14660ef3117c1285e0957aecc16dbf4fbddb2c9 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 24 Jan 2025 11:36:59 +0100
Subject: [PATCH 076/124] Fix and improve translation tests
---
tests/WP_SQLite_Driver_Translation_Tests.php | 974 +++++++++++++++++--
1 file changed, 910 insertions(+), 64 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 1da6334..f7a41b9 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -1,6 +1,7 @@
driver = new WP_SQLite_Driver( new PDO( 'sqlite::memory:' ) );
+ $this->driver = new WP_SQLite_Driver( 'wp', new PDO( 'sqlite::memory:' ) );
}
public function testSelect(): void {
@@ -206,22 +207,104 @@ public function testCreateTable(): void {
'CREATE TABLE t (id INT)'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testCreateTableWithMultipleColumns(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER , "name" TEXT , "score" REAL DEFAULT 0.0 )',
+ 'CREATE TABLE "t" ( "id" INTEGER, "name" TEXT COLLATE NOCASE, "score" REAL DEFAULT \'0.0\' )',
'CREATE TABLE t (id INT, name TEXT, score FLOAT DEFAULT 0.0)'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'name', 2, null, 'YES', 'text', 65535, 65535, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'text', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'score', 3, '0.0', 'YES', 'float', null, null, 12, null, null, null, null, 'float', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testCreateTableWithBasicConstraints(): void {
$this->assertQuery(
'CREATE TABLE "t" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT )',
'CREATE TABLE t (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT)'
);
- // ENGINE is not supported in SQLite.
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'id', 1, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', 'PRI', 'auto_increment', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
+ . " VALUES ('wp', 't', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testCreateTableWithEngine(): void {
+ // ENGINE is not supported in SQLite, we save it in information schema.
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "id" INTEGER )',
+ 'CREATE TABLE t (id INT) ENGINE=MyISAM'
+ );
+
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'MyISAM', 'FIXED', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testCreateTableWithCollate(): void {
+ // COLLATE is not supported in SQLite, we save it in information schema.
$this->assertQuery(
'CREATE TABLE "t" ( "id" INTEGER )',
- 'CREATE TABLE t (id INT) ENGINE=InnoDB'
+ 'CREATE TABLE t (id INT) COLLATE utf8mb4_czech_ci'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_czech_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testCreateTableWithPrimaryKey(): void {
/*
* PRIMARY KEY without AUTOINCREMENT:
* In this case, integer must be represented as INT, not INTEGER. SQLite
@@ -232,34 +315,169 @@ public function testCreateTable(): void {
* https://www.sqlite.org/lang_createtable.html#rowids_and_the_integer_primary_key
*/
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INT PRIMARY KEY )',
+ 'CREATE TABLE "t" ( "id" INT NOT NULL, PRIMARY KEY ("id") )',
'CREATE TABLE t (id INT PRIMARY KEY)'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'id', 1, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', 'PRI', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
+ . " VALUES ('wp', 't', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void {
// With AUTOINCREMENT, we expect "INTEGER".
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT )',
- 'CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT)'
+ 'CREATE TABLE "t1" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT )',
+ 'CREATE TABLE t1 (id INT PRIMARY KEY AUTO_INCREMENT)'
+ );
+
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't1', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't1', 'id', 1, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', 'PRI', 'auto_increment', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
+ . " VALUES ('wp', 't1', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't1'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't1'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't1'",
+ )
);
// In SQLite, PRIMARY KEY must come before AUTOINCREMENT.
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT )',
- 'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY)'
+ 'CREATE TABLE "t2" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT )',
+ 'CREATE TABLE t2 (id INT AUTO_INCREMENT PRIMARY KEY)'
+ );
+
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't2', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't2', 'id', 1, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', 'PRI', 'auto_increment', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
+ . " VALUES ('wp', 't2', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't2'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't2'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't2'",
+ )
);
// In SQLite, AUTOINCREMENT cannot be specified separately from PRIMARY KEY.
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT )',
- 'CREATE TABLE t (id INT AUTO_INCREMENT, PRIMARY KEY(id))'
+ 'CREATE TABLE "t3" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT )',
+ 'CREATE TABLE t3 (id INT AUTO_INCREMENT, PRIMARY KEY(id))'
+ );
+
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't3', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't3', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', 'auto_increment', 'select,insert,update,references', '', '', null)",
+ "SELECT column_name, data_type, is_nullable, character_maximum_length FROM _mysql_information_schema_columns WHERE table_name = 't3' AND column_name IN ('id')",
+ 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
+ . " VALUES ('wp', 't3', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)",
+ "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't3' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't3' AND s.column_name = c.column_name",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't3'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't3'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't3'",
+ )
);
+ }
- // IF NOT EXISTS.
+ // @TODO: IF NOT EXISTS
+ /*public function testCreateTableWithIfNotExists(): void {
$this->assertQuery(
'CREATE TABLE IF NOT EXISTS "t" ( "id" INTEGER )',
'CREATE TABLE IF NOT EXISTS t (id INT)'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ )
+ );
+ }*/
+
+ public function testCreateTableWithInlineUniqueIndexes(): void {
+ $this->assertQuery(
+ array(
+ 'CREATE TABLE "t" ( "id" INTEGER, "name" TEXT COLLATE NOCASE )',
+ 'CREATE UNIQUE INDEX "t__id" ON "t" ("id")',
+ 'CREATE UNIQUE INDEX "t__name" ON "t" ("name")',
+ ),
+ 'CREATE TABLE t (id INT UNIQUE, name TEXT UNIQUE)'
+ );
+
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', 'UNI', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
+ . " VALUES ('wp', 't', 0, 'wp', 'id', 1, 'id', 'A', 0, null, null, 'YES', 'BTREE', '', '', 'YES', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'name', 2, null, 'YES', 'text', 65535, 65535, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'text', 'UNI', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
+ . " VALUES ('wp', 't', 0, 'wp', 'name', 1, 'name', 'A', 0, null, null, 'YES', 'BTREE', '', '', 'YES', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testCreateTableWithStandaloneUniqueIndexes(): void {
+ $this->assertQuery(
+ array(
+ 'CREATE TABLE "t" ( "id" INTEGER, "name" TEXT COLLATE NOCASE )',
+ 'CREATE UNIQUE INDEX "t__id" ON "t" ("id")',
+ 'CREATE UNIQUE INDEX "t__name" ON "t" ("name")',
+ ),
+ 'CREATE TABLE t (id INT, name VARCHAR(100), UNIQUE (id), UNIQUE (name))'
+ );
+
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'name', 2, null, 'YES', 'varchar', 100, 400, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'varchar(100)', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT column_name, data_type, is_nullable, character_maximum_length FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name IN ('id')",
+ 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
+ . " VALUES ('wp', 't', 0, 'wp', 'id', 1, 'id', 'A', 0, null, null, 'YES', 'BTREE', '', '', 'YES', null)",
+ "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
+ "SELECT column_name, data_type, is_nullable, character_maximum_length FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name IN ('name')",
+ 'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
+ . " VALUES ('wp', 't', 0, 'wp', 'name', 1, 'name', 'A', 0, null, null, 'YES', 'BTREE', '', '', 'YES', null)",
+ "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testCreateTableFromSelectQuery(): void {
// CREATE TABLE AS SELECT ...
$this->assertQuery(
'CREATE TABLE "t1" AS SELECT * FROM "t2"',
@@ -272,160 +490,735 @@ public function testCreateTable(): void {
'CREATE TABLE "t1" AS SELECT * FROM "t2"',
'CREATE TABLE t1 SELECT * FROM t2'
);
+ }
- // TEMPORARY.
+ public function testCreateTemporaryTable(): void {
$this->assertQuery(
'CREATE TEMPORARY TABLE "t" ( "id" INTEGER )',
'CREATE TEMPORARY TABLE t (id INT)'
);
- // TEMPORARY & IF NOT EXISTS.
+ // With IF NOT EXISTS.
$this->assertQuery(
'CREATE TEMPORARY TABLE IF NOT EXISTS "t" ( "id" INTEGER )',
'CREATE TEMPORARY TABLE IF NOT EXISTS t (id INT)'
);
}
- public function testAlterTable(): void {
- // Prepare a real table, so we can test multi-operation alter statements.
- // Otherwise, we'd hit and exception and rollback after the first query.
+ public function testAlterTableAddColumn(): void {
+ $this->driver->query( 'CREATE TABLE t (id INT)' );
+ $this->assertQuery(
+ array(
+ 'PRAGMA foreign_keys',
+ 'PRAGMA foreign_keys = OFF',
+ 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER )',
+ 'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
+ 'DROP TABLE "t"',
+ 'ALTER TABLE "" RENAME TO "t"',
+ 'PRAGMA foreign_key_check',
+ 'PRAGMA foreign_keys = ON',
+ ),
+ 'ALTER TABLE t ADD a INT'
+ );
+
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'a', 2, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testAlterTableAddColumnWithNotNull(): void {
+ $this->driver->query( 'CREATE TABLE t (id INT)' );
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT )',
- 'CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT)'
+ array(
+ 'PRAGMA foreign_keys',
+ 'PRAGMA foreign_keys = OFF',
+ 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER NOT NULL )',
+ 'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
+ 'DROP TABLE "t"',
+ 'ALTER TABLE "" RENAME TO "t"',
+ 'PRAGMA foreign_key_check',
+ 'PRAGMA foreign_keys = ON',
+ ),
+ 'ALTER TABLE t ADD a INT NOT NULL'
);
- // ADD COLUMN.
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'a', 2, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testAlterTableAddColumnWithDefault(): void {
+ $this->driver->query( 'CREATE TABLE t (id INT)' );
$this->assertQuery(
- 'ALTER TABLE "t" ADD COLUMN "a" INTEGER',
- 'ALTER TABLE t ADD a INT'
+ array(
+ 'PRAGMA foreign_keys',
+ 'PRAGMA foreign_keys = OFF',
+ 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER DEFAULT \'0\' )',
+ 'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
+ 'DROP TABLE "t"',
+ 'ALTER TABLE "" RENAME TO "t"',
+ 'PRAGMA foreign_key_check',
+ 'PRAGMA foreign_keys = ON',
+ ),
+ 'ALTER TABLE t ADD a INT DEFAULT 0'
+ );
+
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'a', 2, '0', 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testAlterTableAddColumnWithNotNullAndDefault(): void {
+ $this->driver->query( 'CREATE TABLE t (id INT)' );
+ $this->assertQuery(
+ array(
+ 'PRAGMA foreign_keys',
+ 'PRAGMA foreign_keys = OFF',
+ 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER NOT NULL DEFAULT \'0\' )',
+ 'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
+ 'DROP TABLE "t"',
+ 'ALTER TABLE "" RENAME TO "t"',
+ 'PRAGMA foreign_key_check',
+ 'PRAGMA foreign_keys = ON',
+ ),
+ 'ALTER TABLE t ADD a INT NOT NULL DEFAULT 0'
+ );
+
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'a', 2, '0', 'NO', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
);
+ }
- // ADD COLUMN with multiple columns.
+ public function testAlterTableAddMultipleColumns(): void {
+ $this->driver->query( 'CREATE TABLE t (id INT)' );
$this->assertQuery(
array(
- 'ALTER TABLE "t" ADD COLUMN "b" INTEGER',
- 'ALTER TABLE "t" ADD COLUMN "c" TEXT',
- 'ALTER TABLE "t" ADD COLUMN "d" INTEGER',
+ 'PRAGMA foreign_keys',
+ 'PRAGMA foreign_keys = OFF',
+ 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER, "b" TEXT COLLATE NOCASE, "c" INTEGER )',
+ 'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
+ 'DROP TABLE "t"',
+ 'ALTER TABLE "" RENAME TO "t"',
+ 'PRAGMA foreign_key_check',
+ 'PRAGMA foreign_keys = ON',
),
- 'ALTER TABLE t ADD b INT, ADD c TEXT, ADD d BOOL'
+ 'ALTER TABLE t ADD a INT, ADD b TEXT, ADD c BOOL'
);
- // DROP COLUMN.
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'a', 2, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'b', 3, null, 'YES', 'text', 65535, 65535, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'text', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c', 4, null, 'YES', 'tinyint', null, null, 3, 0, null, null, null, 'tinyint(1)', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testAlterTableDropColumn(): void {
+ $this->driver->query( 'CREATE TABLE t (id INT, a TEXT)' );
$this->assertQuery(
- 'ALTER TABLE "t" DROP COLUMN "a"',
+ array(
+ 'PRAGMA foreign_keys',
+ 'PRAGMA foreign_keys = OFF',
+ 'CREATE TABLE "" ( "id" INTEGER )',
+ 'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
+ 'DROP TABLE "t"',
+ 'ALTER TABLE "" RENAME TO "t"',
+ 'PRAGMA foreign_key_check',
+ 'PRAGMA foreign_keys = ON',
+ ),
'ALTER TABLE t DROP a'
);
- // DROP COLUMN with multiple columns.
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "DELETE FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name = 'a'",
+ "DELETE FROM _mysql_information_schema_statistics WHERE table_name = 't' AND column_name = 'a'",
+ "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ ); }
+
+ public function testAlterTableDropMultipleColumns(): void {
+ $this->driver->query( 'CREATE TABLE t (id INT, a INT, b TEXT)' );
+ $this->assertQuery(
+ array(
+ 'PRAGMA foreign_keys',
+ 'PRAGMA foreign_keys = OFF',
+ 'CREATE TABLE "" ( "id" INTEGER )',
+ 'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
+ 'DROP TABLE "t"',
+ 'ALTER TABLE "" RENAME TO "t"',
+ 'PRAGMA foreign_key_check',
+ 'PRAGMA foreign_keys = ON',
+ ),
+ 'ALTER TABLE t DROP a, DROP b'
+ );
+
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "DELETE FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name = 'a'",
+ "DELETE FROM _mysql_information_schema_statistics WHERE table_name = 't' AND column_name = 'a'",
+ "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
+ "DELETE FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name = 'b'",
+ "DELETE FROM _mysql_information_schema_statistics WHERE table_name = 't' AND column_name = 'b'",
+ "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testAlterTableAddAndDropColumns(): void {
+ $this->driver->query( 'CREATE TABLE t (a INT)' );
$this->assertQuery(
array(
- 'ALTER TABLE "t" DROP COLUMN "b"',
- 'ALTER TABLE "t" DROP COLUMN "c"',
+ 'PRAGMA foreign_keys',
+ 'PRAGMA foreign_keys = OFF',
+ 'CREATE TABLE "" ( "b" INTEGER )',
+ 'INSERT INTO "" ("rowid") SELECT "rowid" FROM "t"',
+ 'DROP TABLE "t"',
+ 'ALTER TABLE "" RENAME TO "t"',
+ 'PRAGMA foreign_key_check',
+ 'PRAGMA foreign_keys = ON',
),
- 'ALTER TABLE t DROP b, DROP c'
+ 'ALTER TABLE t ADD b INT, DROP a'
);
- // ADD COLUMN and DROP COLUMN combined.
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'b', 2, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ "DELETE FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name = 'a'",
+ "DELETE FROM _mysql_information_schema_statistics WHERE table_name = 't' AND column_name = 'a'",
+ "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testAlterTableDropAndAddSingleColumn(): void {
+ $this->driver->query( 'CREATE TABLE t (a INT)' );
$this->assertQuery(
array(
- 'ALTER TABLE "t" ADD COLUMN "a" INTEGER',
- 'ALTER TABLE "t" DROP COLUMN "d"',
+ 'PRAGMA foreign_keys',
+ 'PRAGMA foreign_keys = OFF',
+ 'CREATE TABLE "" ( "a" INTEGER )',
+ 'INSERT INTO "" ("rowid") SELECT "rowid" FROM "t"',
+ 'DROP TABLE "t"',
+ 'ALTER TABLE "" RENAME TO "t"',
+ 'PRAGMA foreign_key_check',
+ 'PRAGMA foreign_keys = ON',
),
- 'ALTER TABLE t ADD a INT, DROP d'
+ 'ALTER TABLE t DROP a, ADD a INT'
+ );
+
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ "SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "DELETE FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name = 'a'",
+ "DELETE FROM _mysql_information_schema_statistics WHERE table_name = 't' AND column_name = 'a'",
+ "WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'a', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testBitDataTypes(): void {
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "i1" INTEGER, "i2" INTEGER )',
+ 'CREATE TABLE t (i1 BIT, i2 BIT(10))'
+ );
+
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'i1', 1, null, 'YES', 'bit', null, null, 1, null, null, null, null, 'bit(1)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'i2', 2, null, 'YES', 'bit', null, null, 10, null, null, null, null, 'bit(10)', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
);
}
- public function testDataTypes(): void {
- // Numeric data types.
+ public function testBooleanDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "i1" INTEGER , "i2" INTEGER , "i3" INTEGER )',
- 'CREATE TABLE t (i1 BIT, i2 BOOL, i3 BOOLEAN)'
+ 'CREATE TABLE "t" ( "i1" INTEGER, "i2" INTEGER )',
+ 'CREATE TABLE t (i1 BOOL, i2 BOOLEAN)'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'i1', 1, null, 'YES', 'tinyint', null, null, 3, 0, null, null, null, 'tinyint(1)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'i2', 2, null, 'YES', 'tinyint', null, null, 3, 0, null, null, null, 'tinyint(1)', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testIntegerDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "i1" INTEGER , "i2" INTEGER , "i3" INTEGER , "i4" INTEGER , "i5" INTEGER , "i6" INTEGER )',
+ 'CREATE TABLE "t" ( "i1" INTEGER, "i2" INTEGER, "i3" INTEGER, "i4" INTEGER, "i5" INTEGER, "i6" INTEGER )',
'CREATE TABLE t (i1 TINYINT, i2 SMALLINT, i3 MEDIUMINT, i4 INT, i5 INTEGER, i6 BIGINT)'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'i1', 1, null, 'YES', 'tinyint', null, null, 3, 0, null, null, null, 'tinyint', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'i2', 2, null, 'YES', 'smallint', null, null, 5, 0, null, null, null, 'smallint', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'i3', 3, null, 'YES', 'mediumint', null, null, 7, 0, null, null, null, 'mediumint', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'i4', 4, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'i5', 5, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'i6', 6, null, 'YES', 'bigint', null, null, 19, 0, null, null, null, 'bigint', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testFloatDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "f1" REAL , "f2" REAL , "f3" REAL , "f4" REAL )',
+ 'CREATE TABLE "t" ( "f1" REAL, "f2" REAL, "f3" REAL, "f4" REAL )',
'CREATE TABLE t (f1 FLOAT, f2 DOUBLE, f3 DOUBLE PRECISION, f4 REAL)'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'f1', 1, null, 'YES', 'float', null, null, 12, null, null, null, null, 'float', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'f2', 2, null, 'YES', 'double', null, null, 22, null, null, null, null, 'double', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'f3', 3, null, 'YES', 'double', null, null, 22, null, null, null, null, 'double', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'f4', 4, null, 'YES', 'double', null, null, 22, null, null, null, null, 'double', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testDecimalTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "f1" REAL , "f2" REAL , "f3" REAL , "f4" REAL )',
+ 'CREATE TABLE "t" ( "f1" REAL, "f2" REAL, "f3" REAL, "f4" REAL )',
'CREATE TABLE t (f1 DECIMAL, f2 DEC, f3 FIXED, f4 NUMERIC)'
);
- // String data types.
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'f1', 1, null, 'YES', 'decimal', null, null, 10, 0, null, null, null, 'decimal(10,0)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'f2', 2, null, 'YES', 'decimal', null, null, 10, 0, null, null, null, 'decimal(10,0)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'f3', 3, null, 'YES', 'decimal', null, null, 10, 0, null, null, null, 'decimal(10,0)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'f4', 4, null, 'YES', 'decimal', null, null, 10, 0, null, null, null, 'decimal(10,0)', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testCharDataTypes(): void {
+ $this->assertQuery(
+ 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE t (c1 CHAR, c2 CHAR(10))'
+ );
+
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c1', 1, null, 'YES', 'char', 1, 4, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'char(1)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c2', 2, null, 'YES', 'char', 10, 40, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'char(10)', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testVarcharDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "c1" TEXT , "c2" TEXT , "c3" TEXT , "c4" TEXT )',
- 'CREATE TABLE t (c1 CHAR, c2 VARCHAR(255), c3 CHAR VARYING(255), c4 CHARACTER VARYING(255))'
+ 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE t (c1 VARCHAR(255), c2 CHAR VARYING(255), c3 CHARACTER VARYING(255))'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c1', 1, null, 'YES', 'varchar', 255, 1020, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c2', 2, null, 'YES', 'varchar', 255, 1020, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c3', 3, null, 'YES', 'varchar', 255, 1020, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testNationalCharDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "c1" TEXT , "c2" TEXT )',
- 'CREATE TABLE t (c1 NATIONAL CHAR, c2 NCHAR)'
+ 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE, "c4" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE t (c1 NATIONAL CHAR, c2 NCHAR, c3 NATIONAL CHAR (10), c4 NCHAR(10))'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c1', 1, null, 'YES', 'char', 1, 3, null, null, null, 'utf8', 'utf8_general_ci', 'char(1)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c2', 2, null, 'YES', 'char', 1, 3, null, null, null, 'utf8', 'utf8_general_ci', 'char(1)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c3', 3, null, 'YES', 'char', 10, 30, null, null, null, 'utf8', 'utf8_general_ci', 'char(10)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c4', 4, null, 'YES', 'char', 10, 30, null, null, null, 'utf8', 'utf8_general_ci', 'char(10)', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testNcharVarcharDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "c1" TEXT , "c2" TEXT , "c3" TEXT )',
+ 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE )',
'CREATE TABLE t (c1 NCHAR VARCHAR(255), c2 NCHAR VARYING(255), c3 NVARCHAR(255))'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c1', 1, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c2', 2, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c3', 3, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testNationalVarcharDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "c1" TEXT , "c2" TEXT , "c3" TEXT )',
+ 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE )',
'CREATE TABLE t (c1 NATIONAL VARCHAR(255), c2 NATIONAL CHAR VARYING(255), c3 NATIONAL CHARACTER VARYING(255))'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c1', 1, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c2', 2, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'c3', 3, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testTextDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "t1" TEXT , "t2" TEXT , "t3" TEXT , "t4" TEXT )',
+ 'CREATE TABLE "t" ( "t1" TEXT COLLATE NOCASE, "t2" TEXT COLLATE NOCASE, "t3" TEXT COLLATE NOCASE, "t4" TEXT COLLATE NOCASE )',
'CREATE TABLE t (t1 TINYTEXT, t2 TEXT, t3 MEDIUMTEXT, t4 LONGTEXT)'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 't1', 1, null, 'YES', 'tinytext', 255, 255, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'tinytext', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 't2', 2, null, 'YES', 'text', 65535, 65535, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'text', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 't3', 3, null, 'YES', 'mediumtext', 16777215, 16777215, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'mediumtext', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 't4', 4, null, 'YES', 'longtext', 4294967295, 4294967295, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'longtext', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testEnumDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "e" TEXT )',
+ 'CREATE TABLE "t" ( "e" TEXT COLLATE NOCASE )',
'CREATE TABLE t (e ENUM("a", "b", "c"))'
);
- // Date and time data types.
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'e', 1, null, 'YES', 'enum', 1, 4, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'enum(''a'',''b'',''c'')', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testDateAndTimeDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "d" TEXT , "t" TEXT , "dt" TEXT , "ts" TEXT , "y" TEXT )',
+ 'CREATE TABLE "t" ( "d" TEXT COLLATE NOCASE, "t" TEXT COLLATE NOCASE, "dt" TEXT COLLATE NOCASE, "ts" TEXT COLLATE NOCASE, "y" TEXT COLLATE NOCASE )',
'CREATE TABLE t (d DATE, t TIME, dt DATETIME, ts TIMESTAMP, y YEAR)'
);
- // Binary data types.
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'd', 1, null, 'YES', 'date', null, null, null, null, null, null, null, 'date', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 't', 2, null, 'YES', 'time', null, null, null, null, 0, null, null, 'time', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'dt', 3, null, 'YES', 'datetime', null, null, null, null, 0, null, null, 'datetime', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'ts', 4, null, 'YES', 'timestamp', null, null, null, null, 0, null, null, 'timestamp', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'y', 5, null, 'YES', 'year', null, null, null, null, null, null, null, 'year', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testBinaryDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "b" INTEGER , "v" BLOB )',
+ 'CREATE TABLE "t" ( "b" INTEGER, "v" BLOB )',
'CREATE TABLE t (b BINARY, v VARBINARY(255))'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'b', 1, null, 'YES', 'binary', 1, 1, null, null, null, null, null, 'binary(1)', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'v', 2, null, 'YES', 'varbinary', 255, 255, null, null, null, null, null, 'varbinary(255)', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testBlobDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "b1" BLOB , "b2" BLOB , "b3" BLOB , "b4" BLOB )',
+ 'CREATE TABLE "t" ( "b1" BLOB, "b2" BLOB, "b3" BLOB, "b4" BLOB )',
'CREATE TABLE t (b1 TINYBLOB, b2 BLOB, b3 MEDIUMBLOB, b4 LONGBLOB)'
);
- // Spatial data types.
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'b1', 1, null, 'YES', 'tinyblob', 255, 255, null, null, null, null, null, 'tinyblob', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'b2', 2, null, 'YES', 'blob', 65535, 65535, null, null, null, null, null, 'blob', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'b3', 3, null, 'YES', 'mediumblob', 16777215, 16777215, null, null, null, null, null, 'mediumblob', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'b4', 4, null, 'YES', 'longblob', 4294967295, 4294967295, null, null, null, null, null, 'longblob', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testBasicSpatialDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "g1" TEXT , "g2" TEXT , "g3" TEXT , "g4" TEXT )',
+ 'CREATE TABLE "t" ( "g1" TEXT COLLATE NOCASE, "g2" TEXT COLLATE NOCASE, "g3" TEXT COLLATE NOCASE, "g4" TEXT COLLATE NOCASE )',
'CREATE TABLE t (g1 GEOMETRY, g2 POINT, g3 LINESTRING, g4 POLYGON)'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'g1', 1, null, 'YES', 'geometry', null, null, null, null, null, null, null, 'geometry', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'g2', 2, null, 'YES', 'point', null, null, null, null, null, null, null, 'point', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'g3', 3, null, 'YES', 'linestring', null, null, null, null, null, null, null, 'linestring', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'g4', 4, null, 'YES', 'polygon', null, null, null, null, null, null, null, 'polygon', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testMultiObjectSpatialDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "g1" TEXT , "g2" TEXT , "g3" TEXT )',
+ 'CREATE TABLE "t" ( "g1" TEXT COLLATE NOCASE, "g2" TEXT COLLATE NOCASE, "g3" TEXT COLLATE NOCASE )',
'CREATE TABLE t (g1 MULTIPOINT, g2 MULTILINESTRING, g3 MULTIPOLYGON)'
);
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'g1', 1, null, 'YES', 'multipoint', null, null, null, null, null, null, null, 'multipoint', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'g2', 2, null, 'YES', 'multilinestring', null, null, null, null, null, null, null, 'multilinestring', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'g3', 3, null, 'YES', 'multipolygon', null, null, null, null, null, null, null, 'multipolygon', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testGeometryCollectionDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "g1" TEXT , "g2" TEXT )',
+ 'CREATE TABLE "t" ( "g1" TEXT COLLATE NOCASE, "g2" TEXT COLLATE NOCASE )',
'CREATE TABLE t (g1 GEOMCOLLECTION, g2 GEOMETRYCOLLECTION)'
);
- // SERIAL
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'g1', 1, null, 'YES', 'geomcollection', null, null, null, null, null, null, null, 'geomcollection', '', '', 'select,insert,update,references', '', '', null)",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'g2', 2, null, 'YES', 'geomcollection', null, null, null, null, null, null, null, 'geomcollection', '', '', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
+ }
+
+ public function testSerialDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE )',
+ 'CREATE TABLE "t" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT )',
'CREATE TABLE t (id SERIAL)'
);
+
+ $this->assertExecutedInformationSchemaQueries(
+ array(
+ 'INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation)'
+ . " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
+ 'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
+ . " VALUES ('wp', 't', 'id', 1, null, 'NO', 'bigint', null, null, 20, 0, null, null, null, 'bigint unsigned', 'PRI', 'auto_increment', 'select,insert,update,references', '', '', null)",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
+ )
+ );
}
public function testSystemVariables(): void {
@@ -465,6 +1258,16 @@ private function assertQuery( $expected, string $query ): void {
$executed_queries = array_values( array_slice( $executed_queries, 1, -1, true ) );
}
+ // Remove "information_schema" queries.
+ $executed_queries = array_values(
+ array_filter(
+ $executed_queries,
+ function ( $query ) {
+ return ! str_contains( $query, '_mysql_information_schema_' );
+ }
+ )
+ );
+
// Remove "select changes()" executed after some queries.
if (
count( $executed_queries ) > 1
@@ -475,6 +1278,49 @@ private function assertQuery( $expected, string $query ): void {
if ( ! is_array( $expected ) ) {
$expected = array( $expected );
}
+
+ // Normalize whitespace.
+ foreach ( $executed_queries as $key => $executed_query ) {
+ $executed_queries[ $key ] = trim( preg_replace( '/\s+/', ' ', $executed_query ) );
+ }
+
+ // Normalize temporary table names.
+ foreach ( $executed_queries as $key => $executed_query ) {
+ $executed_queries[ $key ] = preg_replace( '/"_wp_sqlite_tmp_[^"]+"/', '""', $executed_query );
+ }
+
$this->assertSame( $expected, $executed_queries );
}
+
+ private function assertExecutedInformationSchemaQueries( array $expected ): void {
+ // Collect and normalize "information_schema" queries.
+ $queries = array();
+ foreach ( $this->driver->executed_sqlite_queries as $query ) {
+ if ( ! str_contains( $query['sql'], '_mysql_information_schema_' ) ) {
+ continue;
+ }
+
+ // Normalize whitespace.
+ $sql = trim( preg_replace( '/\s+/', ' ', $query['sql'] ) );
+
+ // Inline parameters.
+ $sql = str_replace( '?', '%s', $sql );
+ $queries[] = sprintf(
+ $sql,
+ ...array_map(
+ function ( $param ) {
+ if ( null === $param ) {
+ return 'null';
+ }
+ if ( is_string( $param ) ) {
+ return $this->driver->get_pdo()->quote( $param );
+ }
+ return $param;
+ },
+ $query['params']
+ )
+ );
+ }
+ $this->assertSame( $expected, $queries );
+ }
}
From 3cfaf92aa4aa0f771383d1141d1adb6189c772c6 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 24 Jan 2025 13:13:51 +0100
Subject: [PATCH 077/124] Use correct string literal quotes
---
tests/WP_SQLite_Driver_Translation_Tests.php | 78 +++++++++----------
.../sqlite-ast/class-wp-sqlite-driver.php | 12 +--
2 files changed, 45 insertions(+), 45 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index f7a41b9..10b59a6 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -213,7 +213,7 @@ public function testCreateTable(): void {
. " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -236,7 +236,7 @@ public function testCreateTableWithMultipleColumns(): void {
. " VALUES ('wp', 't', 'name', 2, null, 'YES', 'text', 65535, 65535, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'text', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'score', 3, '0.0', 'YES', 'float', null, null, 12, null, null, null, null, 'float', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -257,7 +257,7 @@ public function testCreateTableWithBasicConstraints(): void {
. " VALUES ('wp', 't', 'id', 1, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', 'PRI', 'auto_increment', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
. " VALUES ('wp', 't', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -277,7 +277,7 @@ public function testCreateTableWithEngine(): void {
. " VALUES ('wp', 't', 'BASE TABLE', 'MyISAM', 'FIXED', 'utf8mb4_general_ci')",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -297,7 +297,7 @@ public function testCreateTableWithCollate(): void {
. " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_czech_ci')",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -327,7 +327,7 @@ public function testCreateTableWithPrimaryKey(): void {
. " VALUES ('wp', 't', 'id', 1, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', 'PRI', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
. " VALUES ('wp', 't', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -349,7 +349,7 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void {
. " VALUES ('wp', 't1', 'id', 1, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', 'PRI', 'auto_increment', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
. " VALUES ('wp', 't1', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't1'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't1'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't1'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't1'",
)
@@ -369,7 +369,7 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void {
. " VALUES ('wp', 't2', 'id', 1, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', 'PRI', 'auto_increment', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
. " VALUES ('wp', 't2', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't2'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't2'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't2'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't2'",
)
@@ -391,7 +391,7 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void {
'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
. " VALUES ('wp', 't3', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)",
"WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't3' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't3' AND s.column_name = c.column_name",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't3'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't3'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't3'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't3'",
)
@@ -437,7 +437,7 @@ public function testCreateTableWithInlineUniqueIndexes(): void {
. " VALUES ('wp', 't', 'name', 2, null, 'YES', 'text', 65535, 65535, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'text', 'UNI', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
. " VALUES ('wp', 't', 0, 'wp', 'name', 1, 'name', 'A', 0, null, null, 'YES', 'BTREE', '', '', 'YES', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -470,7 +470,7 @@ public function testCreateTableWithStandaloneUniqueIndexes(): void {
'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
. " VALUES ('wp', 't', 0, 'wp', 'name', 1, 'name', 'A', 0, null, null, 'YES', 'BTREE', '', '', 'YES', null)",
"WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -527,7 +527,7 @@ public function testAlterTableAddColumn(): void {
"SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'a', 2, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -556,7 +556,7 @@ public function testAlterTableAddColumnWithNotNull(): void {
"SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'a', 2, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -585,7 +585,7 @@ public function testAlterTableAddColumnWithDefault(): void {
"SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'a', 2, '0', 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -614,7 +614,7 @@ public function testAlterTableAddColumnWithNotNullAndDefault(): void {
"SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'a', 2, '0', 'NO', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -649,7 +649,7 @@ public function testAlterTableAddMultipleColumns(): void {
"SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'c', 4, null, 'YES', 'tinyint', null, null, 3, 0, null, null, null, 'tinyint(1)', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -678,7 +678,7 @@ public function testAlterTableDropColumn(): void {
"DELETE FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name = 'a'",
"DELETE FROM _mysql_information_schema_statistics WHERE table_name = 't' AND column_name = 'a'",
"WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -709,7 +709,7 @@ public function testAlterTableDropMultipleColumns(): void {
"DELETE FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name = 'b'",
"DELETE FROM _mysql_information_schema_statistics WHERE table_name = 't' AND column_name = 'b'",
"WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -741,7 +741,7 @@ public function testAlterTableAddAndDropColumns(): void {
"DELETE FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name = 'a'",
"DELETE FROM _mysql_information_schema_statistics WHERE table_name = 't' AND column_name = 'a'",
"WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -773,7 +773,7 @@ public function testAlterTableDropAndAddSingleColumn(): void {
"SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'a', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -794,7 +794,7 @@ public function testBitDataTypes(): void {
. " VALUES ('wp', 't', 'i1', 1, null, 'YES', 'bit', null, null, 1, null, null, null, null, 'bit(1)', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'i2', 2, null, 'YES', 'bit', null, null, 10, null, null, null, null, 'bit(10)', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -815,7 +815,7 @@ public function testBooleanDataTypes(): void {
. " VALUES ('wp', 't', 'i1', 1, null, 'YES', 'tinyint', null, null, 3, 0, null, null, null, 'tinyint(1)', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'i2', 2, null, 'YES', 'tinyint', null, null, 3, 0, null, null, null, 'tinyint(1)', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -844,7 +844,7 @@ public function testIntegerDataTypes(): void {
. " VALUES ('wp', 't', 'i5', 5, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'i6', 6, null, 'YES', 'bigint', null, null, 19, 0, null, null, null, 'bigint', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -869,7 +869,7 @@ public function testFloatDataTypes(): void {
. " VALUES ('wp', 't', 'f3', 3, null, 'YES', 'double', null, null, 22, null, null, null, null, 'double', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'f4', 4, null, 'YES', 'double', null, null, 22, null, null, null, null, 'double', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -894,7 +894,7 @@ public function testDecimalTypes(): void {
. " VALUES ('wp', 't', 'f3', 3, null, 'YES', 'decimal', null, null, 10, 0, null, null, null, 'decimal(10,0)', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'f4', 4, null, 'YES', 'decimal', null, null, 10, 0, null, null, null, 'decimal(10,0)', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -915,7 +915,7 @@ public function testCharDataTypes(): void {
. " VALUES ('wp', 't', 'c1', 1, null, 'YES', 'char', 1, 4, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'char(1)', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'c2', 2, null, 'YES', 'char', 10, 40, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'char(10)', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -938,7 +938,7 @@ public function testVarcharDataTypes(): void {
. " VALUES ('wp', 't', 'c2', 2, null, 'YES', 'varchar', 255, 1020, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'c3', 3, null, 'YES', 'varchar', 255, 1020, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -963,7 +963,7 @@ public function testNationalCharDataTypes(): void {
. " VALUES ('wp', 't', 'c3', 3, null, 'YES', 'char', 10, 30, null, null, null, 'utf8', 'utf8_general_ci', 'char(10)', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'c4', 4, null, 'YES', 'char', 10, 30, null, null, null, 'utf8', 'utf8_general_ci', 'char(10)', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -986,7 +986,7 @@ public function testNcharVarcharDataTypes(): void {
. " VALUES ('wp', 't', 'c2', 2, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'c3', 3, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -1009,7 +1009,7 @@ public function testNationalVarcharDataTypes(): void {
. " VALUES ('wp', 't', 'c2', 2, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'c3', 3, null, 'YES', 'varchar', 255, 765, null, null, null, 'utf8', 'utf8_general_ci', 'varchar(255)', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -1034,7 +1034,7 @@ public function testTextDataTypes(): void {
. " VALUES ('wp', 't', 't3', 3, null, 'YES', 'mediumtext', 16777215, 16777215, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'mediumtext', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 't4', 4, null, 'YES', 'longtext', 4294967295, 4294967295, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'longtext', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -1053,7 +1053,7 @@ public function testEnumDataTypes(): void {
. " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'e', 1, null, 'YES', 'enum', 1, 4, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'enum(''a'',''b'',''c'')', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -1080,7 +1080,7 @@ public function testDateAndTimeDataTypes(): void {
. " VALUES ('wp', 't', 'ts', 4, null, 'YES', 'timestamp', null, null, null, null, 0, null, null, 'timestamp', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'y', 5, null, 'YES', 'year', null, null, null, null, null, null, null, 'year', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -1101,7 +1101,7 @@ public function testBinaryDataTypes(): void {
. " VALUES ('wp', 't', 'b', 1, null, 'YES', 'binary', 1, 1, null, null, null, null, null, 'binary(1)', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'v', 2, null, 'YES', 'varbinary', 255, 255, null, null, null, null, null, 'varbinary(255)', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -1126,7 +1126,7 @@ public function testBlobDataTypes(): void {
. " VALUES ('wp', 't', 'b3', 3, null, 'YES', 'mediumblob', 16777215, 16777215, null, null, null, null, null, 'mediumblob', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'b4', 4, null, 'YES', 'longblob', 4294967295, 4294967295, null, null, null, null, null, 'longblob', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -1151,7 +1151,7 @@ public function testBasicSpatialDataTypes(): void {
. " VALUES ('wp', 't', 'g3', 3, null, 'YES', 'linestring', null, null, null, null, null, null, null, 'linestring', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'g4', 4, null, 'YES', 'polygon', null, null, null, null, null, null, null, 'polygon', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -1174,7 +1174,7 @@ public function testMultiObjectSpatialDataTypes(): void {
. " VALUES ('wp', 't', 'g2', 2, null, 'YES', 'multilinestring', null, null, null, null, null, null, null, 'multilinestring', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'g3', 3, null, 'YES', 'multipolygon', null, null, null, null, null, null, null, 'multipolygon', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -1195,7 +1195,7 @@ public function testGeometryCollectionDataTypes(): void {
. " VALUES ('wp', 't', 'g1', 1, null, 'YES', 'geomcollection', null, null, null, null, null, null, null, 'geomcollection', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'g2', 2, null, 'YES', 'geomcollection', null, null, null, null, null, null, null, 'geomcollection', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
@@ -1214,7 +1214,7 @@ public function testSerialDataTypes(): void {
. " VALUES ('wp', 't', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'id', 1, null, 'NO', 'bigint', null, null, 20, 0, null, null, null, 'bigint unsigned', 'PRI', 'auto_increment', 'select,insert,update,references', '', '', null)",
- "SELECT * FROM _mysql_information_schema_tables WHERE table_type = \"BASE TABLE\" AND table_schema = 'wp' AND table_name = 't'",
+ "SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't'",
)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index fc67ad1..ea0a3c0 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -2005,13 +2005,13 @@ private function translate_datetime_literal( string $value ): string {
private function get_sqlite_create_table_statement( string $table_name, ?string $new_table_name = null ): array {
// 1. Get table info.
$table_info = $this->execute_sqlite_query(
- '
+ "
SELECT *
FROM _mysql_information_schema_tables
- WHERE table_type = "BASE TABLE"
+ WHERE table_type = 'BASE TABLE'
AND table_schema = ?
AND table_name = ?
- ',
+ ",
array( $this->db_name, $table_name )
)->fetch( PDO::FETCH_ASSOC );
@@ -2161,13 +2161,13 @@ function ( $column ) {
private function get_mysql_create_table_statement( string $table_name ): ?string {
// 1. Get table info.
$table_info = $this->execute_sqlite_query(
- '
+ "
SELECT *
FROM _mysql_information_schema_tables
- WHERE table_type = "BASE TABLE"
+ WHERE table_type = 'BASE TABLE'
AND table_schema = ?
AND table_name = ?
- ',
+ ",
array( $this->db_name, $table_name )
)->fetch( PDO::FETCH_ASSOC );
From b8b0bef0c8fc0d4d5e0e288e7d973ec3a1177218 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 24 Jan 2025 14:27:40 +0100
Subject: [PATCH 078/124] Use table schema value in all information schema
queries
---
tests/WP_SQLite_Driver_Translation_Tests.php | 44 +++++++++----------
...s-wp-sqlite-information-schema-builder.php | 39 ++++++++++------
2 files changed, 47 insertions(+), 36 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 10b59a6..1e36069 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -387,7 +387,7 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void {
. " VALUES ('wp', 't3', 'BASE TABLE', 'InnoDB', 'DYNAMIC', 'utf8mb4_general_ci')",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't3', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', 'auto_increment', 'select,insert,update,references', '', '', null)",
- "SELECT column_name, data_type, is_nullable, character_maximum_length FROM _mysql_information_schema_columns WHERE table_name = 't3' AND column_name IN ('id')",
+ "SELECT column_name, data_type, is_nullable, character_maximum_length FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't3' AND column_name IN ('id')",
'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
. " VALUES ('wp', 't3', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)",
"WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't3' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't3' AND s.column_name = c.column_name",
@@ -462,11 +462,11 @@ public function testCreateTableWithStandaloneUniqueIndexes(): void {
. " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'name', 2, null, 'YES', 'varchar', 100, 400, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'varchar(100)', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT column_name, data_type, is_nullable, character_maximum_length FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name IN ('id')",
+ "SELECT column_name, data_type, is_nullable, character_maximum_length FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't' AND column_name IN ('id')",
'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
. " VALUES ('wp', 't', 0, 'wp', 'id', 1, 'id', 'A', 0, null, null, 'YES', 'BTREE', '', '', 'YES', null)",
"WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
- "SELECT column_name, data_type, is_nullable, character_maximum_length FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name IN ('name')",
+ "SELECT column_name, data_type, is_nullable, character_maximum_length FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't' AND column_name IN ('name')",
'INSERT INTO _mysql_information_schema_statistics (table_schema, table_name, non_unique, index_schema, index_name, seq_in_index, column_name, collation, cardinality, sub_part, packed, nullable, index_type, comment, index_comment, is_visible, expression)'
. " VALUES ('wp', 't', 0, 'wp', 'name', 1, 'name', 'A', 0, null, null, 'YES', 'BTREE', '', '', 'YES', null)",
"WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
@@ -524,7 +524,7 @@ public function testAlterTableAddColumn(): void {
$this->assertExecutedInformationSchemaQueries(
array(
"SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
- "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'a', 2, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
"SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
@@ -553,7 +553,7 @@ public function testAlterTableAddColumnWithNotNull(): void {
$this->assertExecutedInformationSchemaQueries(
array(
"SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
- "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'a', 2, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
"SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
@@ -582,7 +582,7 @@ public function testAlterTableAddColumnWithDefault(): void {
$this->assertExecutedInformationSchemaQueries(
array(
"SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
- "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'a', 2, '0', 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
"SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
@@ -611,7 +611,7 @@ public function testAlterTableAddColumnWithNotNullAndDefault(): void {
$this->assertExecutedInformationSchemaQueries(
array(
"SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
- "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'a', 2, '0', 'NO', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
"SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
@@ -640,13 +640,13 @@ public function testAlterTableAddMultipleColumns(): void {
$this->assertExecutedInformationSchemaQueries(
array(
"SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
- "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'a', 2, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'b', 3, null, 'YES', 'text', 65535, 65535, null, null, null, 'utf8mb4', 'utf8mb4_general_ci', 'text', '', '', 'select,insert,update,references', '', '', null)",
- "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'c', 4, null, 'YES', 'tinyint', null, null, 3, 0, null, null, null, 'tinyint(1)', '', '', 'select,insert,update,references', '', '', null)",
"SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
@@ -675,8 +675,8 @@ public function testAlterTableDropColumn(): void {
$this->assertExecutedInformationSchemaQueries(
array(
"SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
- "DELETE FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name = 'a'",
- "DELETE FROM _mysql_information_schema_statistics WHERE table_name = 't' AND column_name = 'a'",
+ "DELETE FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'",
+ "DELETE FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'",
"WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
"SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
@@ -703,11 +703,11 @@ public function testAlterTableDropMultipleColumns(): void {
$this->assertExecutedInformationSchemaQueries(
array(
"SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
- "DELETE FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name = 'a'",
- "DELETE FROM _mysql_information_schema_statistics WHERE table_name = 't' AND column_name = 'a'",
+ "DELETE FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'",
+ "DELETE FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'",
"WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
- "DELETE FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name = 'b'",
- "DELETE FROM _mysql_information_schema_statistics WHERE table_name = 't' AND column_name = 'b'",
+ "DELETE FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'b'",
+ "DELETE FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'b'",
"WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
"SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
@@ -735,11 +735,11 @@ public function testAlterTableAddAndDropColumns(): void {
$this->assertExecutedInformationSchemaQueries(
array(
"SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
- "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'b', 2, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
- "DELETE FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name = 'a'",
- "DELETE FROM _mysql_information_schema_statistics WHERE table_name = 't' AND column_name = 'a'",
+ "DELETE FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'",
+ "DELETE FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'",
"WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
"SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
"SELECT * FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
@@ -767,10 +767,10 @@ public function testAlterTableDropAndAddSingleColumn(): void {
$this->assertExecutedInformationSchemaQueries(
array(
"SELECT COLUMN_NAME FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
- "DELETE FROM _mysql_information_schema_columns WHERE table_name = 't' AND column_name = 'a'",
- "DELETE FROM _mysql_information_schema_statistics WHERE table_name = 't' AND column_name = 'a'",
+ "DELETE FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'",
+ "DELETE FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' AND column_name = 'a'",
"WITH s AS ( SELECT column_name, CASE WHEN MAX(index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(non_unique = 0 AND seq_in_index = 1) THEN 'UNI' WHEN MAX(seq_in_index = 1) THEN 'MUL' ELSE '' END AS column_key FROM _mysql_information_schema_statistics WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE _mysql_information_schema_columns AS c SET column_key = s.column_key, is_nullable = IIF(s.column_key = 'PRI', 'NO', c.is_nullable) FROM s WHERE c.table_schema = 'wp' AND c.table_name = 't' AND s.column_name = c.column_name",
- "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = 't'",
+ "SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_schema = 'wp' AND table_name = 't'",
'INSERT INTO _mysql_information_schema_columns (table_schema, table_name, column_name, ordinal_position, column_default, is_nullable, data_type, character_maximum_length, character_octet_length, numeric_precision, numeric_scale, datetime_precision, character_set_name, collation_name, column_type, column_key, extra, privileges, column_comment, generation_expression, srs_id)'
. " VALUES ('wp', 't', 'a', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)",
"SELECT * FROM _mysql_information_schema_tables WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'",
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index fdc81d2..27a7acd 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -542,8 +542,13 @@ public function record_drop_table( WP_Parser_Node $node ): void {
private function record_add_column( string $table_name, string $column_name, WP_Parser_Node $node ): void {
$position = $this->query(
- 'SELECT MAX(ordinal_position) FROM _mysql_information_schema_columns WHERE table_name = ?',
- array( $table_name )
+ '
+ SELECT MAX(ordinal_position)
+ FROM _mysql_information_schema_columns
+ WHERE table_schema = ?
+ AND table_name = ?
+ ',
+ array( $this->db_name, $table_name )
)->fetchColumn();
$column_data = $this->extract_column_data( $table_name, $column_name, $node, (int) $position + 1 );
@@ -566,8 +571,9 @@ private function record_change_column(
'_mysql_information_schema_columns',
$column_data,
array(
- 'table_name' => $table_name,
- 'column_name' => $column_name,
+ 'table_schema' => $this->db_name,
+ 'table_name' => $table_name,
+ 'column_name' => $column_name,
)
);
@@ -579,8 +585,9 @@ private function record_change_column(
'column_name' => $new_column_name,
),
array(
- 'table_name' => $table_name,
- 'column_name' => $column_name,
+ 'table_schema' => $this->db_name,
+ 'table_name' => $table_name,
+ 'column_name' => $column_name,
)
);
}
@@ -611,8 +618,9 @@ private function record_drop_column( $table_name, $column_name ): void {
$this->delete_values(
'_mysql_information_schema_columns',
array(
- 'table_name' => $table_name,
- 'column_name' => $column_name,
+ 'table_schema' => $this->db_name,
+ 'table_name' => $table_name,
+ 'column_name' => $column_name,
)
);
@@ -632,8 +640,9 @@ private function record_drop_column( $table_name, $column_name ): void {
$this->delete_values(
'_mysql_information_schema_statistics',
array(
- 'table_name' => $table_name,
- 'column_name' => $column_name,
+ 'table_schema' => $this->db_name,
+ 'table_name' => $table_name,
+ 'column_name' => $column_name,
)
);
@@ -646,8 +655,9 @@ private function record_drop_index( string $table_name, string $index_name ): vo
$this->delete_values(
'_mysql_information_schema_statistics',
array(
- 'table_name' => $table_name,
- 'index_name' => $index_name,
+ 'table_schema' => $this->db_name,
+ 'table_name' => $table_name,
+ 'index_name' => $index_name,
)
);
$this->sync_column_key_info( $table_name );
@@ -692,10 +702,11 @@ private function record_add_constraint( string $table_name, WP_Parser_Node $node
'
SELECT column_name, data_type, is_nullable, character_maximum_length
FROM _mysql_information_schema_columns
- WHERE table_name = ?
+ WHERE table_schema = ?
+ AND table_name = ?
AND column_name IN (' . implode( ',', array_fill( 0, count( $column_names ), '?' ) ) . ')
',
- array_merge( array( $table_name ), $column_names )
+ array_merge( array( $this->db_name, $table_name ), $column_names )
)->fetchAll( PDO::FETCH_ASSOC );
} else {
$column_info = array();
From d8059798d52f787ea2380622c98a9962faecc446 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 24 Jan 2025 15:07:52 +0100
Subject: [PATCH 079/124] Remove prototype classes
---
tests/WP_SQLite_Driver_Tests.php | 4 -
tests/WP_SQLite_Driver_Translation_Tests.php | 2 -
.../class-wp-sqlite-driver-prototype.php | 670 ------------------
.../sqlite-ast/class-wp-sqlite-expression.php | 39 -
.../class-wp-sqlite-query-builder.php | 43 --
.../class-wp-sqlite-token-factory.php | 400 -----------
.../sqlite-ast/class-wp-sqlite-token.php | 19 -
7 files changed, 1177 deletions(-)
delete mode 100644 wp-includes/sqlite-ast/class-wp-sqlite-driver-prototype.php
delete mode 100644 wp-includes/sqlite-ast/class-wp-sqlite-expression.php
delete mode 100644 wp-includes/sqlite-ast/class-wp-sqlite-query-builder.php
delete mode 100644 wp-includes/sqlite-ast/class-wp-sqlite-token-factory.php
delete mode 100644 wp-includes/sqlite-ast/class-wp-sqlite-token.php
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index ea3c805..35defde 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -3,10 +3,6 @@
require_once __DIR__ . '/WP_SQLite_Translator_Tests.php';
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
-require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-expression.php';
-require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-token-factory.php';
-require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-token.php';
-require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-query-builder.php';
use PHPUnit\Framework\TestCase;
use WIP\WP_SQLite_Driver;
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 1e36069..f71b007 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -2,8 +2,6 @@
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
-require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-token-factory.php';
-require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-token.php';
use PHPUnit\Framework\TestCase;
use WIP\WP_SQLite_Driver;
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver-prototype.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver-prototype.php
deleted file mode 100644
index 9296051..0000000
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver-prototype.php
+++ /dev/null
@@ -1,670 +0,0 @@
-pdo = $pdo;
- $this->grammar = $grammar;
-
- $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
- $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
- $pdo->setAttribute( PDO::ATTR_TIMEOUT, 5 );
- }
-
- public function query( $query ) {
- $this->has_sql_calc_found_rows = false;
- $this->has_found_rows_call = false;
- $this->last_calc_rows_result = null;
-
- $lexer = new WP_MySQL_Lexer( $query );
- $tokens = $lexer->remaining_tokens();
-
- $parser = new WP_MySQL_Parser( $this->grammar, $tokens );
- $ast = $parser->parse();
- $expr = $this->translate_query( $ast );
- //$expr = $this->rewrite_sql_calc_found_rows( $expr );
-
- if ( null === $expr ) {
- return false;
- }
-
- $sqlite_query = WP_SQLite_Query_Builder::stringify( $expr );
-
- // Returning the query just for now for testing. In the end, we'll
- // run it and return the SQLite interaction result.
- //return $sqlite_query;
-
- if ( ! $sqlite_query ) {
- return false;
- }
-
- $is_select = (bool) $ast->get_descendant( 'selectStatement' );
- $statement = $this->pdo->prepare( $sqlite_query );
- $return_value = $statement->execute();
- $this->results = $return_value;
- if ( $is_select ) {
- $this->results = $statement->fetchAll( PDO::FETCH_OBJ );
- }
- return $return_value;
- }
-
- public function get_error_message() {
- }
-
- public function get_query_results() {
- return $this->results;
- }
-
- private function rewrite_sql_calc_found_rows( WP_SQLite_Expression $expr ) {
- if ( $this->has_found_rows_call && ! $this->has_sql_calc_found_rows && null === $this->last_calc_rows_result ) {
- throw new Exception( 'FOUND_ROWS() called without SQL_CALC_FOUND_ROWS' );
- }
-
- if ( $this->has_sql_calc_found_rows ) {
- $expr_to_run = $expr;
- if ( $this->has_found_rows_call ) {
- $expr_without_found_rows = new WP_SQLite_Expression( array() );
- foreach ( $expr->elements as $k => $element ) {
- if ( WP_SQLite_Token::TYPE_IDENTIFIER === $element->type && 'FOUND_ROWS' === $element->value ) {
- $expr_without_found_rows->add_token(
- WP_SQLite_Token_Factory::value( 0 )
- );
- } else {
- $expr_without_found_rows->add_token( $element );
- }
- }
- $expr_to_run = $expr_without_found_rows;
- }
-
- // ...remove the LIMIT clause...
- $query = 'SELECT COUNT(*) as cnt FROM (' . WP_SQLite_Query_Builder::stringify( $expr_to_run ) . ');';
-
- // ...run $query...
- // $result = ...
- // $this->last_calc_rows_result = $result['cnt'];
- }
-
- if ( ! $this->has_found_rows_call ) {
- return $expr;
- }
-
- $expr_with_found_rows_result = new WP_SQLite_Expression( array() );
- foreach ( $expr->elements as $k => $element ) {
- if ( WP_SQLite_Token::TYPE_IDENTIFIER === $element->type && 'FOUND_ROWS' === $element->value ) {
- $expr_with_found_rows_result->add_token(
- WP_SQLite_Token_Factory::value( $this->last_calc_rows_result )
- );
- } else {
- $expr_with_found_rows_result->add_token( $element );
- }
- }
- return $expr_with_found_rows_result;
- }
-
- private function translate_query( $ast ) {
- if ( null === $ast ) {
- return null;
- }
-
- if ( $ast instanceof WP_MySQL_Token ) {
- $token = $ast;
- switch ( $token->id ) {
- case WP_MySQL_Lexer::EOF:
- return new WP_SQLite_Expression( array() );
-
- case WP_MySQL_Lexer::IDENTIFIER:
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::identifier(
- trim( $token->text, '`"' )
- ),
- )
- );
-
- default:
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( $token->text ),
- )
- );
- }
- }
-
- if ( ! ( $ast instanceof WP_Parser_Node ) ) {
- throw new Exception( 'translate_query only accepts WP_MySQL_Token and WP_Parser_Node instances' );
- }
-
- $rule_name = $ast->rule_name;
-
- switch ( $rule_name ) {
- case 'indexHintList':
- // SQLite doesn't support index hints. Let's skip them.
- return null;
-
- case 'querySpecOption':
- $token = $ast->get_token();
- switch ( $token->type ) {
- case WP_MySQL_Lexer::ALL_SYMBOL:
- case WP_MySQL_Lexer::DISTINCT_SYMBOL:
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( $token->text ),
- )
- );
- case WP_MySQL_Lexer::SQL_CALC_FOUND_ROWS_SYMBOL:
- $this->has_sql_calc_found_rows = true;
- // Fall through to default.
- default:
- // we'll need to run the current SQL query without any
- // LIMIT clause, and then substitute the FOUND_ROWS()
- // function with a literal number of rows found.
- return new WP_SQLite_Expression( array() );
- }
- // Otherwise, fall through.
-
- case 'fromClause':
- // Skip `FROM DUAL`. We only care about a singular
- // FROM DUAL statement, as FROM mytable, DUAL is a syntax
- // error.
- if (
- $ast->has_token( WP_MySQL_Lexer::DUAL_SYMBOL ) &&
- ! $ast->has_child( 'tableReferenceList' )
- ) {
- return null;
- }
- // Otherwise, fall through.
-
- case 'selectOption':
- case 'interval':
- case 'intervalTimeStamp':
- case 'bitExpr':
- case 'boolPri':
- case 'lockStrengh':
- case 'orderList':
- case 'simpleExpr':
- case 'columnRef':
- case 'exprIs':
- case 'exprAnd':
- case 'primaryExprCompare':
- case 'fieldIdentifier':
- case 'dotIdentifier':
- case 'identifier':
- case 'literal':
- case 'joinedTable':
- case 'nullLiteral':
- case 'boolLiteral':
- case 'numLiteral':
- case 'textLiteral':
- case 'predicate':
- case 'predicateExprBetween':
- case 'primaryExprPredicate':
- case 'pureIdentifier':
- case 'unambiguousIdentifier':
- case 'qualifiedIdentifier':
- case 'query':
- case 'queryExpression':
- case 'queryExpressionBody':
- case 'queryExpressionParens':
- case 'queryPrimary':
- case 'querySpecification':
- case 'queryTerm':
- case 'selectAlias':
- case 'selectItem':
- case 'selectItemList':
- case 'selectStatement':
- case 'simpleExprColumnRef':
- case 'simpleExprFunction':
- case 'outerJoinType':
- case 'simpleExprSubQuery':
- case 'simpleExprLiteral':
- case 'compOp':
- case 'simpleExprList':
- case 'simpleStatement':
- case 'subquery':
- case 'exprList':
- case 'expr':
- case 'tableReferenceList':
- case 'tableReference':
- case 'tableRef':
- case 'tableAlias':
- case 'tableFactor':
- case 'singleTable':
- case 'udfExprList':
- case 'udfExpr':
- case 'withClause':
- case 'whereClause':
- case 'commonTableExpression':
- case 'derivedTable':
- case 'columnRefOrLiteral':
- case 'orderClause':
- case 'groupByClause':
- case 'lockingClauseList':
- case 'lockingClause':
- case 'havingClause':
- case 'direction':
- case 'orderExpression':
- $child_expressions = array();
- foreach ( $ast->children as $child ) {
- $child_expressions[] = $this->translate_query( $child );
- }
- return new WP_SQLite_Expression( $child_expressions );
-
- case 'textStringLiteral':
- return new WP_SQLite_Expression(
- array(
- $ast->has_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT ) ?
- WP_SQLite_Token_Factory::double_quoted_value( $ast->get_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT )->text ) : false,
- $ast->has_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ) ?
- WP_SQLite_Token_Factory::raw( $ast->get_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT )->text ) : false,
- )
- );
-
- case 'functionCall':
- return $this->translate_function_call( $ast );
-
- case 'runtimeFunctionCall':
- return $this->translate_runtime_function_call( $ast );
-
- default:
- return null;
- // var_dump(count($ast->children));
- // foreach($ast->children as $child) {
- // var_dump(get_class($child));
- // echo $child->getText();
- // echo "\n\n";
- // }
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw(
- $rule_name
- ),
- )
- );
- }
- }
-
- private function translate_runtime_function_call( $ast ): WP_SQLite_Expression {
- $name_token = $ast->children[0];
-
- switch ( strtoupper( $name_token->text ) ) {
- case 'ADDDATE':
- case 'DATE_ADD':
- $args = $ast->get_children( 'expr' );
- $interval = $ast->get_child( 'interval' );
- $timespan = $interval->get_child( 'intervalTimeStamp' )->get_token()->text;
- return WP_SQLite_Token_Factory::create_function(
- 'DATETIME',
- array(
- $this->translate_query( $args[0] ),
- new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::value( '+' ),
- WP_SQLite_Token_Factory::raw( '||' ),
- $this->translate_query( $args[1] ),
- WP_SQLite_Token_Factory::raw( '||' ),
- WP_SQLite_Token_Factory::value( $timespan ),
- )
- ),
- )
- );
-
- case 'DATE_SUB':
- // return new WP_SQLite_Expression([
- // SQLiteTokenFactory::raw("DATETIME("),
- // $args[0],
- // SQLiteTokenFactory::raw(", '-'"),
- // $args[1],
- // SQLiteTokenFactory::raw(" days')")
- // ]);
-
- case 'VALUES':
- $column = $ast->get_child()->get_descendant( 'pureIdentifier' );
- if ( ! $column ) {
- throw new Exception( 'VALUES() calls without explicit column names are unsupported' );
- }
-
- $colname = $column->get_token()->extract_value();
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( 'excluded.' ),
- WP_SQLite_Token_Factory::identifier( $colname ),
- )
- );
- default:
- throw new Exception( 'Unsupported function: ' . $name_token->text );
- }
- }
-
- private function translate_function_call( $function_call_tree ): WP_SQLite_Expression {
- $name = $function_call_tree->get_child( 'pureIdentifier' )->get_token()->text;
- $args = array();
- foreach ( $function_call_tree->get_child( 'udfExprList' )->get_children() as $node ) {
- $args[] = $this->translate_query( $node );
- }
- switch ( strtoupper( $name ) ) {
- case 'ABS':
- case 'ACOS':
- case 'ASIN':
- case 'ATAN':
- case 'ATAN2':
- case 'COS':
- case 'DEGREES':
- case 'TRIM':
- case 'EXP':
- case 'MAX':
- case 'MIN':
- case 'FLOOR':
- case 'RADIANS':
- case 'ROUND':
- case 'SIN':
- case 'SQRT':
- case 'TAN':
- case 'TRUNCATE':
- case 'RANDOM':
- case 'PI':
- case 'LTRIM':
- case 'RTRIM':
- return WP_SQLite_Token_Factory::create_function( $name, $args );
-
- case 'CEIL':
- case 'CEILING':
- return WP_SQLite_Token_Factory::create_function( 'CEIL', $args );
-
- case 'COT':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( '1 / ' ),
- WP_SQLite_Token_Factory::create_function( 'TAN', $args ),
- )
- );
-
- case 'LN':
- case 'LOG':
- case 'LOG2':
- return WP_SQLite_Token_Factory::create_function( 'LOG', $args );
-
- case 'LOG10':
- return WP_SQLite_Token_Factory::create_function( 'LOG10', $args );
-
- // case 'MOD':
- // return $this->transformBinaryOperation([
- // 'operator' => '%',
- // 'left' => $args[0],
- // 'right' => $args[1]
- // ]);
-
- case 'POW':
- case 'POWER':
- return WP_SQLite_Token_Factory::create_function( 'POW', $args );
-
- // String functions
- case 'ASCII':
- return WP_SQLite_Token_Factory::create_function( 'UNICODE', $args );
- case 'CHAR_LENGTH':
- case 'LENGTH':
- return WP_SQLite_Token_Factory::create_function( 'LENGTH', $args );
- case 'CONCAT':
- $concated = array( WP_SQLite_Token_Factory::raw( '(' ) );
- foreach ( $args as $k => $arg ) {
- $concated[] = $arg;
- if ( $k < count( $args ) - 1 ) {
- $concated[] = WP_SQLite_Token_Factory::raw( '||' );
- }
- }
- $concated[] = WP_SQLite_Token_Factory::raw( ')' );
- return new WP_SQLite_Expression( $concated );
- // case 'CONCAT_WS':
- // return new WP_SQLite_Expression([
- // SQLiteTokenFactory::raw("REPLACE("),
- // implode(" || ", array_slice($args, 1)),
- // SQLiteTokenFactory::raw(", '', "),
- // $args[0],
- // SQLiteTokenFactory::raw(")")
- // ]);
- case 'INSTR':
- return WP_SQLite_Token_Factory::create_function( 'INSTR', $args );
- case 'LCASE':
- case 'LOWER':
- return WP_SQLite_Token_Factory::create_function( 'LOWER', $args );
- case 'LEFT':
- return WP_SQLite_Token_Factory::create_function(
- 'SUBSTR',
- array(
- $args[0],
- '1',
- $args[1],
- )
- );
- case 'LOCATE':
- return WP_SQLite_Token_Factory::create_function(
- 'INSTR',
- array(
- $args[1],
- $args[0],
- )
- );
- case 'REPEAT':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "REPLACE(CHAR(32), ' ', " ),
- $args[0],
- WP_SQLite_Token_Factory::raw( ')' ),
- )
- );
-
- case 'REPLACE':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( 'REPLACE(' ),
- implode( ', ', $args ),
- WP_SQLite_Token_Factory::raw( ')' ),
- )
- );
- case 'RIGHT':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( 'SUBSTR(' ),
- $args[0],
- WP_SQLite_Token_Factory::raw( ', -(' ),
- $args[1],
- WP_SQLite_Token_Factory::raw( '))' ),
- )
- );
- case 'SPACE':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "REPLACE(CHAR(32), ' ', '')" ),
- )
- );
- case 'SUBSTRING':
- case 'SUBSTR':
- return WP_SQLite_Token_Factory::create_function( 'SUBSTR', $args );
- case 'UCASE':
- case 'UPPER':
- return WP_SQLite_Token_Factory::create_function( 'UPPER', $args );
-
- case 'DATE_FORMAT':
- $mysql_date_format_to_sqlite_strftime = array(
- '%a' => '%D',
- '%b' => '%M',
- '%c' => '%n',
- '%D' => '%jS',
- '%d' => '%d',
- '%e' => '%j',
- '%H' => '%H',
- '%h' => '%h',
- '%I' => '%h',
- '%i' => '%M',
- '%j' => '%z',
- '%k' => '%G',
- '%l' => '%g',
- '%M' => '%F',
- '%m' => '%m',
- '%p' => '%A',
- '%r' => '%h:%i:%s %A',
- '%S' => '%s',
- '%s' => '%s',
- '%T' => '%H:%i:%s',
- '%U' => '%W',
- '%u' => '%W',
- '%V' => '%W',
- '%v' => '%W',
- '%W' => '%l',
- '%w' => '%w',
- '%X' => '%Y',
- '%x' => '%o',
- '%Y' => '%Y',
- '%y' => '%y',
- );
- // @TODO: Implement as user defined function to avoid
- // rewriting something that may be an expression as a string
- $format = $args[1]->elements[0]->value;
- $new_format = strtr( $format, $mysql_date_format_to_sqlite_strftime );
-
- return WP_SQLite_Token_Factory::create_function(
- 'STRFTIME',
- array(
- new WP_SQLite_Expression( array( WP_SQLite_Token_Factory::raw( $new_format ) ) ),
- new WP_SQLite_Expression( array( $args[0] ) ),
- )
- );
- case 'DATEDIFF':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::create_function( 'JULIANDAY', array( $args[0] ) ),
- WP_SQLite_Token_Factory::raw( ' - ' ),
- WP_SQLite_Token_Factory::create_function( 'JULIANDAY', array( $args[1] ) ),
- )
- );
- case 'DAYNAME':
- return WP_SQLite_Token_Factory::create_function(
- 'STRFTIME',
- array_merge( array( '%w' ), $args )
- );
- case 'DAY':
- case 'DAYOFMONTH':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%d' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
- )
- );
- case 'DAYOFWEEK':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%w' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") + 1 AS INTEGER'" ),
- )
- );
- case 'DAYOFYEAR':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%j' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
- )
- );
- case 'HOUR':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%H' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
- )
- );
- case 'MINUTE':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%M' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
- )
- );
- case 'MONTH':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%m' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
- )
- );
- case 'MONTHNAME':
- return WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%m' ), $args ) );
- case 'NOW':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( 'CURRENT_TIMESTAMP()' ),
- )
- );
- case 'SECOND':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%S' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
- )
- );
- case 'TIMESTAMP':
- return new WP_SQLite_Expression(
- array_merge(
- array( WP_SQLite_Token_Factory::raw( 'DATETIME(' ) ),
- $args,
- array( WP_SQLite_Token_Factory::raw( ')' ) )
- )
- );
- case 'YEAR':
- return new WP_SQLite_Expression(
- array(
- WP_SQLite_Token_Factory::raw( "CAST('" ),
- WP_SQLite_Token_Factory::create_function( 'STRFTIME', array_merge( array( '%Y' ), $args ) ),
- WP_SQLite_Token_Factory::raw( ") AS INTEGER'" ),
- )
- );
- case 'FOUND_ROWS':
- $this->has_found_rows_call = true;
- return new WP_SQLite_Expression(
- array(
- // Post-processed in handleSqlCalcFoundRows()
- WP_SQLite_Token_Factory::raw( 'FOUND_ROWS' ),
- )
- );
- default:
- throw new Exception( 'Unsupported function: ' . $name );
- }
- }
-}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-expression.php b/wp-includes/sqlite-ast/class-wp-sqlite-expression.php
deleted file mode 100644
index c0161de..0000000
--- a/wp-includes/sqlite-ast/class-wp-sqlite-expression.php
+++ /dev/null
@@ -1,39 +0,0 @@
-elements );
- } else {
- $new_elements[] = $element;
- }
- }
- $this->elements = $new_elements;
- }
-
- public function get_tokens() {
- return $this->elements;
- }
-
- public function add_token( WP_SQLite_Token $token ) {
- $this->elements[] = $token;
- }
-
- public function add_tokens( array $tokens ) {
- foreach ( $tokens as $token ) {
- $this->add_token( $token );
- }
- }
-
- public function add_expression( $expression ) {
- $this->add_token( $expression );
- }
-}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-query-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-query-builder.php
deleted file mode 100644
index 53f05ea..0000000
--- a/wp-includes/sqlite-ast/class-wp-sqlite-query-builder.php
+++ /dev/null
@@ -1,43 +0,0 @@
-build_query();
- }
-
- public function __construct( WP_SQLite_Expression $expression ) {
- $this->expression = $expression;
- }
-
- public function build_query(): string {
- $query_parts = array();
- foreach ( $this->expression->get_tokens() as $element ) {
- if ( $element instanceof WP_SQLite_Token ) {
- $query_parts[] = $this->process_token( $element );
- } elseif ( $element instanceof WP_SQLite_Expression ) {
- $query_parts[] = '(' . ( new self( $element ) )->build_query() . ')';
- }
- }
- return implode( ' ', $query_parts );
- }
-
- private function process_token( WP_SQLite_Token $token ): string {
- switch ( $token->type ) {
- case WP_SQLite_Token::TYPE_OPERATOR:
- case WP_SQLite_Token::TYPE_RAW:
- case WP_SQLite_Token::TYPE_VALUE:
- return $token->value;
- case WP_SQLite_Token::TYPE_IDENTIFIER:
- return '"' . str_replace( '"', '""', $token->value ) . '"';
- default:
- throw new InvalidArgumentException( 'Unknown token type: ' . $token->type );
- }
- }
-}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-token-factory.php b/wp-includes/sqlite-ast/class-wp-sqlite-token-factory.php
deleted file mode 100644
index 28b458f..0000000
--- a/wp-includes/sqlite-ast/class-wp-sqlite-token-factory.php
+++ /dev/null
@@ -1,400 +0,0 @@
-',
- '!=',
- '<',
- '<=',
- '>',
- '>=',
- ';',
- );
-
- private static $functions = array(
- 'ABS' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'AVG' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'COUNT' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'MAX' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'MIN' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'ROUND' => array(
- 'argCount' => 2,
- 'optionalArgs' => 1,
- ),
- 'SUM' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'LENGTH' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'UPPER' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'LOWER' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'COALESCE' => array(
- 'argCount' => 2,
- 'optionalArgs' => PHP_INT_MAX,
- ),
- 'SUBSTR' => array(
- 'argCount' => 3,
- 'optionalArgs' => 1,
- ),
- 'REPLACE' => array(
- 'argCount' => 3,
- 'optionalArgs' => 0,
- ),
- 'TRIM' => array(
- 'argCount' => 3,
- 'optionalArgs' => 2,
- ),
- 'DATE' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'TIME' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'DATETIME' => array(
- 'argCount' => 2,
- 'optionalArgs' => 1,
- ),
- 'JULIANDAY' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'STRFTIME' => array(
- 'argCount' => 2,
- 'optionalArgs' => 0,
- ),
- 'RANDOM' => array(
- 'argCount' => 0,
- 'optionalArgs' => 0,
- ),
- 'RANDOMBLOB' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'NULLIF' => array(
- 'argCount' => 2,
- 'optionalArgs' => 0,
- ),
- 'IFNULL' => array(
- 'argCount' => 2,
- 'optionalArgs' => 0,
- ),
- 'INSTR' => array(
- 'argCount' => 2,
- 'optionalArgs' => 0,
- ),
- 'HEX' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'QUOTE' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'LIKE' => array(
- 'argCount' => 2,
- 'optionalArgs' => 1,
- ),
- 'GLOB' => array(
- 'argCount' => 2,
- 'optionalArgs' => 0,
- ),
- 'CHAR' => array(
- 'argCount' => 1,
- 'optionalArgs' => PHP_INT_MAX,
- ),
- 'UNICODE' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'TOTAL' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'ZEROBLOB' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'PRINTF' => array(
- 'argCount' => 2,
- 'optionalArgs' => PHP_INT_MAX,
- ),
- 'LTRIM' => array(
- 'argCount' => 2,
- 'optionalArgs' => 1,
- ),
- 'RTRIM' => array(
- 'argCount' => 2,
- 'optionalArgs' => 1,
- ),
- 'BLOB' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'GROUP_CONCAT' => array(
- 'argCount' => 1,
- 'optionalArgs' => 1,
- ),
- 'JSON' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'JSON_ARRAY' => array(
- 'argCount' => 1,
- 'optionalArgs' => PHP_INT_MAX,
- ),
- 'JSON_OBJECT' => array(
- 'argCount' => 1,
- 'optionalArgs' => PHP_INT_MAX,
- ),
- 'JSON_QUOTE' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'JSON_VALID' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'JSON_ARRAY_LENGTH' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'JSON_EXTRACT' => array(
- 'argCount' => 2,
- 'optionalArgs' => PHP_INT_MAX,
- ),
- 'JSON_INSERT' => array(
- 'argCount' => 2,
- 'optionalArgs' => PHP_INT_MAX,
- ),
- 'JSON_REPLACE' => array(
- 'argCount' => 2,
- 'optionalArgs' => PHP_INT_MAX,
- ),
- 'JSON_SET' => array(
- 'argCount' => 2,
- 'optionalArgs' => PHP_INT_MAX,
- ),
- 'JSON_PATCH' => array(
- 'argCount' => 2,
- 'optionalArgs' => 0,
- ),
- 'JSON_REMOVE' => array(
- 'argCount' => 1,
- 'optionalArgs' => PHP_INT_MAX,
- ),
- 'JSON_TYPE' => array(
- 'argCount' => 1,
- 'optionalArgs' => PHP_INT_MAX,
- ),
- 'JSON_DEPTH' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'JSON_KEYS' => array(
- 'argCount' => 1,
- 'optionalArgs' => 1,
- ),
- 'JSON_GROUP_ARRAY' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- 'JSON_GROUP_OBJECT' => array(
- 'argCount' => 1,
- 'optionalArgs' => 0,
- ),
- );
-
- public static function register_function( string $name, int $arg_count, int $optional_args = 0 ): void {
- self::$functions[ strtoupper( $name ) ] = array(
- 'argCount' => $arg_count,
- 'optionalArgs' => $optional_args,
- );
- }
-
- public static function raw( string $value ): WP_SQLite_Token {
- return self::create( WP_SQLite_Token::TYPE_RAW, $value );
- }
-
- public static function identifier( string $value ): WP_SQLite_Token {
- return self::create( WP_SQLite_Token::TYPE_IDENTIFIER, $value );
- }
-
- public static function value( $value ): WP_SQLite_Token {
- return self::create( WP_SQLite_Token::TYPE_VALUE, self::escape_value( $value ) );
- }
-
- public static function double_quoted_value( $value ): WP_SQLite_Token {
- $value = substr( $value, 1, -1 );
- $value = str_replace( '\"', '"', $value );
- $value = str_replace( '""', '"', $value );
- return self::create( WP_SQLite_Token::TYPE_VALUE, self::escape_value( $value ) );
- }
-
- public static function operator( string $value ): WP_SQLite_Token {
- $upper_value = strtoupper( $value );
- if ( ! in_array( $upper_value, self::$valid_operators, true ) ) {
- throw new InvalidArgumentException( "Invalid SQLite operator or keyword: $value" );
- }
- return self::create( WP_SQLite_Token::TYPE_OPERATOR, $upper_value );
- }
-
- public static function create_function( string $name, array $expressions ): WP_SQLite_Expression {
- $upper_name = strtoupper( $name );
- if ( ! isset( self::$functions[ $upper_name ] ) ) {
- throw new InvalidArgumentException( "Unknown SQLite function: $name" );
- }
-
- $function_spec = self::$functions[ $upper_name ];
- $min_args = $function_spec['argCount'] - $function_spec['optionalArgs'];
- $max_args = $function_spec['argCount'];
-
- if ( count( $expressions ) < $min_args || count( $expressions ) > $max_args ) {
- throw new InvalidArgumentException(
- "Function $name expects between $min_args and $max_args arguments, " .
- count( $expressions ) . ' given.'
- );
- }
-
- $tokens = array();
- $tokens[] = self::raw( $upper_name );
- $tokens[] = self::raw( '(' );
-
- foreach ( $expressions as $index => $expression ) {
- if ( $index > 0 ) {
- $tokens[] = self::raw( ',' );
- }
- if ( ! $expression instanceof Expression ) {
- throw new InvalidArgumentException( 'All arguments must be instances of Expression' );
- }
- $tokens = array_merge( $tokens, $expression->elements );
- }
-
- $tokens[] = self::raw( ')' );
-
- return new WP_SQLite_Expression( $tokens );
- }
-
- private static function create( string $type, string $value ): WP_SQLite_Token {
- if ( ! in_array( $type, self::$valid_types, true ) ) {
- throw new InvalidArgumentException( "Invalid token type: $type" );
- }
- return new WP_SQLite_Token( $type, $value );
- }
-
- private static function escape_value( $value ): string {
- if ( is_string( $value ) ) {
- // Ensure the string is valid UTF-8, replace invalid characters with an empty string
- $value = mb_convert_encoding( $value, 'UTF-8', 'UTF-8' );
-
- // Escape single quotes by doubling them
- $value = str_replace( "'", "''", $value );
-
- // Escape backslashes by doubling them
- $value = str_replace( '\\', '\\\\', $value );
-
- // Remove null characters
- $value = str_replace( "\0", '', $value );
-
- // Return the escaped string enclosed in single quotes
- return "'" . $value . "'";
- } elseif ( is_int( $value ) || is_float( $value ) ) {
- return (string) $value;
- } elseif ( is_bool( $value ) ) {
- return $value ? '1' : '0';
- } elseif ( is_null( $value ) ) {
- return 'NULL';
- } else {
- throw new InvalidArgumentException( 'Unsupported value type: ' . gettype( $value ) );
- }
- }
-}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-token.php b/wp-includes/sqlite-ast/class-wp-sqlite-token.php
deleted file mode 100644
index d3c2415..0000000
--- a/wp-includes/sqlite-ast/class-wp-sqlite-token.php
+++ /dev/null
@@ -1,19 +0,0 @@
-type = $type;
- $this->value = $value;
- }
-}
From b6d900b05e6a335e3d5f21abda2b5fc19eac2e62 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 24 Jan 2025 15:10:36 +0100
Subject: [PATCH 080/124] Remove WIP namespacing (the new classes don't
conflict with the existing ones)
---
tests/WP_SQLite_Driver_Tests.php | 1 -
tests/WP_SQLite_Driver_Translation_Tests.php | 1 -
tests/tools/dump-sqlite-query.php | 2 --
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 15 ---------------
...class-wp-sqlite-information-schema-builder.php | 9 ---------
5 files changed, 28 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 35defde..8af200c 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -5,7 +5,6 @@
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
use PHPUnit\Framework\TestCase;
-use WIP\WP_SQLite_Driver;
class WP_SQLite_Driver_Tests extends TestCase {
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index f71b007..cf12866 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -4,7 +4,6 @@
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
use PHPUnit\Framework\TestCase;
-use WIP\WP_SQLite_Driver;
class WP_SQLite_Driver_Translation_Tests extends TestCase {
const GRAMMAR_PATH = __DIR__ . '/../wp-includes/mysql/mysql-grammar.php';
diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php
index 158d79d..56718f0 100644
--- a/tests/tools/dump-sqlite-query.php
+++ b/tests/tools/dump-sqlite-query.php
@@ -14,8 +14,6 @@
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-token.php';
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-query-builder.php';
-use WIP\WP_SQLite_Driver;
-
$driver = new WP_SQLite_Driver( new PDO( 'sqlite::memory:' ) );
$query = "SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id WHERE t1.name = 'abc'";
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index ea0a3c0..c9b8956 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1,20 +1,5 @@
Date: Fri, 24 Jan 2025 21:13:52 +0100
Subject: [PATCH 081/124] Enable usage of PDO in phpcs
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 11 ++++++++---
.../class-wp-sqlite-information-schema-builder.php | 4 +++-
2 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index c9b8956..255adac 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1,5 +1,10 @@
getCode();
if ( self::SQLITE_BUSY === $status || self::SQLITE_LOCKED === $status ) {
@@ -414,7 +419,7 @@ public function __construct( string $db_name, ?PDO $pdo = null ) {
WP_SQLite_PDO_User_Defined_Functions::register_for( $pdo );
// MySQL data comes across stringified by default.
- $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
+ $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
$this->pdo = $pdo;
@@ -480,7 +485,7 @@ public function get_affected_rows() {
* @throws Exception If the query could not run.
* @throws PDOException If the translated query could not run.
*/
- public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) { // phpcs:ignore WordPress.DB.RestrictedClasses
+ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) {
$this->flush();
if ( function_exists( 'apply_filters' ) ) {
/**
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index e5fc1b3..9022fe9 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -698,7 +698,9 @@ private function record_add_constraint( string $table_name, WP_Parser_Node $node
AND column_name IN (' . implode( ',', array_fill( 0, count( $column_names ), '?' ) ) . ')
',
array_merge( array( $this->db_name, $table_name ), $column_names )
- )->fetchAll( PDO::FETCH_ASSOC );
+ )->fetchAll(
+ PDO::FETCH_ASSOC // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
+ );
} else {
$column_info = array();
}
From c564fc962a524b05d4989eb36e3a2fbb90a7ca7f Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Mon, 27 Jan 2025 11:25:44 +0100
Subject: [PATCH 082/124] Add a feature flag to use the new driver
---
wp-includes/sqlite/class-wp-sqlite-db.php | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php
index ed80e19..bb993db 100644
--- a/wp-includes/sqlite/class-wp-sqlite-db.php
+++ b/wp-includes/sqlite/class-wp-sqlite-db.php
@@ -229,7 +229,20 @@ public function db_connect( $allow_bail = true ) {
if ( isset( $GLOBALS['@pdo'] ) ) {
$pdo = $GLOBALS['@pdo'];
}
- $this->dbh = new WP_SQLite_Translator( $pdo );
+ if ( defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ) {
+ require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-grammar.php';
+ require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser.php';
+ require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-node.php';
+ require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-token.php';
+ require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php';
+ require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php';
+ require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php';
+ require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
+ require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
+ $this->dbh = new WP_SQLite_Driver( 'wp', $pdo );
+ } else {
+ $this->dbh = new WP_SQLite_Translator( $pdo );
+ }
$this->last_error = $this->dbh->get_error_message();
if ( $this->last_error ) {
return false;
From c7e121f4bcf5b94f4d997a93e81ce932d578ccb3 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 28 Jan 2025 12:51:54 +0100
Subject: [PATCH 083/124] Use strict SQLite tables to validate data types
See: https://www.sqlite.org/stricttables.html
---
tests/WP_SQLite_Driver_Translation_Tests.php | 88 +++++++++----------
.../sqlite-ast/class-wp-sqlite-driver.php | 4 +-
2 files changed, 46 insertions(+), 46 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index cf12866..2497721 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -200,7 +200,7 @@ public function testDelete(): void {
public function testCreateTable(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER )',
+ 'CREATE TABLE "t" ( "id" INTEGER ) STRICT',
'CREATE TABLE t (id INT)'
);
@@ -219,7 +219,7 @@ public function testCreateTable(): void {
public function testCreateTableWithMultipleColumns(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER, "name" TEXT COLLATE NOCASE, "score" REAL DEFAULT \'0.0\' )',
+ 'CREATE TABLE "t" ( "id" INTEGER, "name" TEXT COLLATE NOCASE, "score" REAL DEFAULT \'0.0\' ) STRICT',
'CREATE TABLE t (id INT, name TEXT, score FLOAT DEFAULT 0.0)'
);
@@ -242,7 +242,7 @@ public function testCreateTableWithMultipleColumns(): void {
public function testCreateTableWithBasicConstraints(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT )',
+ 'CREATE TABLE "t" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
'CREATE TABLE t (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT)'
);
@@ -264,7 +264,7 @@ public function testCreateTableWithBasicConstraints(): void {
public function testCreateTableWithEngine(): void {
// ENGINE is not supported in SQLite, we save it in information schema.
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER )',
+ 'CREATE TABLE "t" ( "id" INTEGER ) STRICT',
'CREATE TABLE t (id INT) ENGINE=MyISAM'
);
@@ -284,7 +284,7 @@ public function testCreateTableWithEngine(): void {
public function testCreateTableWithCollate(): void {
// COLLATE is not supported in SQLite, we save it in information schema.
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER )',
+ 'CREATE TABLE "t" ( "id" INTEGER ) STRICT',
'CREATE TABLE t (id INT) COLLATE utf8mb4_czech_ci'
);
@@ -312,7 +312,7 @@ public function testCreateTableWithPrimaryKey(): void {
* https://www.sqlite.org/lang_createtable.html#rowids_and_the_integer_primary_key
*/
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INT NOT NULL, PRIMARY KEY ("id") )',
+ 'CREATE TABLE "t" ( "id" INT NOT NULL, PRIMARY KEY ("id") ) STRICT',
'CREATE TABLE t (id INT PRIMARY KEY)'
);
@@ -334,7 +334,7 @@ public function testCreateTableWithPrimaryKey(): void {
public function testCreateTableWithPrimaryKeyAndAutoincrement(): void {
// With AUTOINCREMENT, we expect "INTEGER".
$this->assertQuery(
- 'CREATE TABLE "t1" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT )',
+ 'CREATE TABLE "t1" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
'CREATE TABLE t1 (id INT PRIMARY KEY AUTO_INCREMENT)'
);
@@ -354,7 +354,7 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void {
// In SQLite, PRIMARY KEY must come before AUTOINCREMENT.
$this->assertQuery(
- 'CREATE TABLE "t2" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT )',
+ 'CREATE TABLE "t2" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
'CREATE TABLE t2 (id INT AUTO_INCREMENT PRIMARY KEY)'
);
@@ -374,7 +374,7 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void {
// In SQLite, AUTOINCREMENT cannot be specified separately from PRIMARY KEY.
$this->assertQuery(
- 'CREATE TABLE "t3" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT )',
+ 'CREATE TABLE "t3" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
'CREATE TABLE t3 (id INT AUTO_INCREMENT, PRIMARY KEY(id))'
);
@@ -398,7 +398,7 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void {
// @TODO: IF NOT EXISTS
/*public function testCreateTableWithIfNotExists(): void {
$this->assertQuery(
- 'CREATE TABLE IF NOT EXISTS "t" ( "id" INTEGER )',
+ 'CREATE TABLE IF NOT EXISTS "t" ( "id" INTEGER ) STRICT',
'CREATE TABLE IF NOT EXISTS t (id INT)'
);
@@ -415,7 +415,7 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void {
public function testCreateTableWithInlineUniqueIndexes(): void {
$this->assertQuery(
array(
- 'CREATE TABLE "t" ( "id" INTEGER, "name" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE "t" ( "id" INTEGER, "name" TEXT COLLATE NOCASE ) STRICT',
'CREATE UNIQUE INDEX "t__id" ON "t" ("id")',
'CREATE UNIQUE INDEX "t__name" ON "t" ("name")',
),
@@ -444,7 +444,7 @@ public function testCreateTableWithInlineUniqueIndexes(): void {
public function testCreateTableWithStandaloneUniqueIndexes(): void {
$this->assertQuery(
array(
- 'CREATE TABLE "t" ( "id" INTEGER, "name" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE "t" ( "id" INTEGER, "name" TEXT COLLATE NOCASE ) STRICT',
'CREATE UNIQUE INDEX "t__id" ON "t" ("id")',
'CREATE UNIQUE INDEX "t__name" ON "t" ("name")',
),
@@ -477,27 +477,27 @@ public function testCreateTableWithStandaloneUniqueIndexes(): void {
public function testCreateTableFromSelectQuery(): void {
// CREATE TABLE AS SELECT ...
$this->assertQuery(
- 'CREATE TABLE "t1" AS SELECT * FROM "t2"',
+ 'CREATE TABLE "t1" AS SELECT * FROM "t2" STRICT',
'CREATE TABLE t1 AS SELECT * FROM t2'
);
// CREATE TABLE SELECT ...
// The "AS" keyword is optional in MySQL, but required in SQLite.
$this->assertQuery(
- 'CREATE TABLE "t1" AS SELECT * FROM "t2"',
+ 'CREATE TABLE "t1" AS SELECT * FROM "t2" STRICT',
'CREATE TABLE t1 SELECT * FROM t2'
);
}
public function testCreateTemporaryTable(): void {
$this->assertQuery(
- 'CREATE TEMPORARY TABLE "t" ( "id" INTEGER )',
+ 'CREATE TEMPORARY TABLE "t" ( "id" INTEGER ) STRICT',
'CREATE TEMPORARY TABLE t (id INT)'
);
// With IF NOT EXISTS.
$this->assertQuery(
- 'CREATE TEMPORARY TABLE IF NOT EXISTS "t" ( "id" INTEGER )',
+ 'CREATE TEMPORARY TABLE IF NOT EXISTS "t" ( "id" INTEGER ) STRICT',
'CREATE TEMPORARY TABLE IF NOT EXISTS t (id INT)'
);
}
@@ -508,7 +508,7 @@ public function testAlterTableAddColumn(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER )',
+ 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER ) STRICT',
'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
'DROP TABLE "t"',
'ALTER TABLE "" RENAME TO "t"',
@@ -537,7 +537,7 @@ public function testAlterTableAddColumnWithNotNull(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER NOT NULL )',
+ 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER NOT NULL ) STRICT',
'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
'DROP TABLE "t"',
'ALTER TABLE "" RENAME TO "t"',
@@ -566,7 +566,7 @@ public function testAlterTableAddColumnWithDefault(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER DEFAULT \'0\' )',
+ 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER DEFAULT \'0\' ) STRICT',
'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
'DROP TABLE "t"',
'ALTER TABLE "" RENAME TO "t"',
@@ -595,7 +595,7 @@ public function testAlterTableAddColumnWithNotNullAndDefault(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER NOT NULL DEFAULT \'0\' )',
+ 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER NOT NULL DEFAULT \'0\' ) STRICT',
'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
'DROP TABLE "t"',
'ALTER TABLE "" RENAME TO "t"',
@@ -624,7 +624,7 @@ public function testAlterTableAddMultipleColumns(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER, "b" TEXT COLLATE NOCASE, "c" INTEGER )',
+ 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER, "b" TEXT COLLATE NOCASE, "c" INTEGER ) STRICT',
'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
'DROP TABLE "t"',
'ALTER TABLE "" RENAME TO "t"',
@@ -659,7 +659,7 @@ public function testAlterTableDropColumn(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "id" INTEGER )',
+ 'CREATE TABLE "" ( "id" INTEGER ) STRICT',
'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
'DROP TABLE "t"',
'ALTER TABLE "" RENAME TO "t"',
@@ -687,7 +687,7 @@ public function testAlterTableDropMultipleColumns(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "id" INTEGER )',
+ 'CREATE TABLE "" ( "id" INTEGER ) STRICT',
'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
'DROP TABLE "t"',
'ALTER TABLE "" RENAME TO "t"',
@@ -719,7 +719,7 @@ public function testAlterTableAddAndDropColumns(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "b" INTEGER )',
+ 'CREATE TABLE "" ( "b" INTEGER ) STRICT',
'INSERT INTO "" ("rowid") SELECT "rowid" FROM "t"',
'DROP TABLE "t"',
'ALTER TABLE "" RENAME TO "t"',
@@ -751,7 +751,7 @@ public function testAlterTableDropAndAddSingleColumn(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "a" INTEGER )',
+ 'CREATE TABLE "" ( "a" INTEGER ) STRICT',
'INSERT INTO "" ("rowid") SELECT "rowid" FROM "t"',
'DROP TABLE "t"',
'ALTER TABLE "" RENAME TO "t"',
@@ -779,7 +779,7 @@ public function testAlterTableDropAndAddSingleColumn(): void {
public function testBitDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "i1" INTEGER, "i2" INTEGER )',
+ 'CREATE TABLE "t" ( "i1" INTEGER, "i2" INTEGER ) STRICT',
'CREATE TABLE t (i1 BIT, i2 BIT(10))'
);
@@ -800,7 +800,7 @@ public function testBitDataTypes(): void {
public function testBooleanDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "i1" INTEGER, "i2" INTEGER )',
+ 'CREATE TABLE "t" ( "i1" INTEGER, "i2" INTEGER ) STRICT',
'CREATE TABLE t (i1 BOOL, i2 BOOLEAN)'
);
@@ -821,7 +821,7 @@ public function testBooleanDataTypes(): void {
public function testIntegerDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "i1" INTEGER, "i2" INTEGER, "i3" INTEGER, "i4" INTEGER, "i5" INTEGER, "i6" INTEGER )',
+ 'CREATE TABLE "t" ( "i1" INTEGER, "i2" INTEGER, "i3" INTEGER, "i4" INTEGER, "i5" INTEGER, "i6" INTEGER ) STRICT',
'CREATE TABLE t (i1 TINYINT, i2 SMALLINT, i3 MEDIUMINT, i4 INT, i5 INTEGER, i6 BIGINT)'
);
@@ -850,7 +850,7 @@ public function testIntegerDataTypes(): void {
public function testFloatDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "f1" REAL, "f2" REAL, "f3" REAL, "f4" REAL )',
+ 'CREATE TABLE "t" ( "f1" REAL, "f2" REAL, "f3" REAL, "f4" REAL ) STRICT',
'CREATE TABLE t (f1 FLOAT, f2 DOUBLE, f3 DOUBLE PRECISION, f4 REAL)'
);
@@ -875,7 +875,7 @@ public function testFloatDataTypes(): void {
public function testDecimalTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "f1" REAL, "f2" REAL, "f3" REAL, "f4" REAL )',
+ 'CREATE TABLE "t" ( "f1" REAL, "f2" REAL, "f3" REAL, "f4" REAL ) STRICT',
'CREATE TABLE t (f1 DECIMAL, f2 DEC, f3 FIXED, f4 NUMERIC)'
);
@@ -900,7 +900,7 @@ public function testDecimalTypes(): void {
public function testCharDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (c1 CHAR, c2 CHAR(10))'
);
@@ -921,7 +921,7 @@ public function testCharDataTypes(): void {
public function testVarcharDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (c1 VARCHAR(255), c2 CHAR VARYING(255), c3 CHARACTER VARYING(255))'
);
@@ -944,7 +944,7 @@ public function testVarcharDataTypes(): void {
public function testNationalCharDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE, "c4" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE, "c4" TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (c1 NATIONAL CHAR, c2 NCHAR, c3 NATIONAL CHAR (10), c4 NCHAR(10))'
);
@@ -969,7 +969,7 @@ public function testNationalCharDataTypes(): void {
public function testNcharVarcharDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (c1 NCHAR VARCHAR(255), c2 NCHAR VARYING(255), c3 NVARCHAR(255))'
);
@@ -992,7 +992,7 @@ public function testNcharVarcharDataTypes(): void {
public function testNationalVarcharDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (c1 NATIONAL VARCHAR(255), c2 NATIONAL CHAR VARYING(255), c3 NATIONAL CHARACTER VARYING(255))'
);
@@ -1015,7 +1015,7 @@ public function testNationalVarcharDataTypes(): void {
public function testTextDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "t1" TEXT COLLATE NOCASE, "t2" TEXT COLLATE NOCASE, "t3" TEXT COLLATE NOCASE, "t4" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE "t" ( "t1" TEXT COLLATE NOCASE, "t2" TEXT COLLATE NOCASE, "t3" TEXT COLLATE NOCASE, "t4" TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (t1 TINYTEXT, t2 TEXT, t3 MEDIUMTEXT, t4 LONGTEXT)'
);
@@ -1040,7 +1040,7 @@ public function testTextDataTypes(): void {
public function testEnumDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "e" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE "t" ( "e" TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (e ENUM("a", "b", "c"))'
);
@@ -1059,7 +1059,7 @@ public function testEnumDataTypes(): void {
public function testDateAndTimeDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "d" TEXT COLLATE NOCASE, "t" TEXT COLLATE NOCASE, "dt" TEXT COLLATE NOCASE, "ts" TEXT COLLATE NOCASE, "y" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE "t" ( "d" TEXT COLLATE NOCASE, "t" TEXT COLLATE NOCASE, "dt" TEXT COLLATE NOCASE, "ts" TEXT COLLATE NOCASE, "y" TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (d DATE, t TIME, dt DATETIME, ts TIMESTAMP, y YEAR)'
);
@@ -1086,7 +1086,7 @@ public function testDateAndTimeDataTypes(): void {
public function testBinaryDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "b" INTEGER, "v" BLOB )',
+ 'CREATE TABLE "t" ( "b" INTEGER, "v" BLOB ) STRICT',
'CREATE TABLE t (b BINARY, v VARBINARY(255))'
);
@@ -1107,7 +1107,7 @@ public function testBinaryDataTypes(): void {
public function testBlobDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "b1" BLOB, "b2" BLOB, "b3" BLOB, "b4" BLOB )',
+ 'CREATE TABLE "t" ( "b1" BLOB, "b2" BLOB, "b3" BLOB, "b4" BLOB ) STRICT',
'CREATE TABLE t (b1 TINYBLOB, b2 BLOB, b3 MEDIUMBLOB, b4 LONGBLOB)'
);
@@ -1132,7 +1132,7 @@ public function testBlobDataTypes(): void {
public function testBasicSpatialDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "g1" TEXT COLLATE NOCASE, "g2" TEXT COLLATE NOCASE, "g3" TEXT COLLATE NOCASE, "g4" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE "t" ( "g1" TEXT COLLATE NOCASE, "g2" TEXT COLLATE NOCASE, "g3" TEXT COLLATE NOCASE, "g4" TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (g1 GEOMETRY, g2 POINT, g3 LINESTRING, g4 POLYGON)'
);
@@ -1157,7 +1157,7 @@ public function testBasicSpatialDataTypes(): void {
public function testMultiObjectSpatialDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "g1" TEXT COLLATE NOCASE, "g2" TEXT COLLATE NOCASE, "g3" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE "t" ( "g1" TEXT COLLATE NOCASE, "g2" TEXT COLLATE NOCASE, "g3" TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (g1 MULTIPOINT, g2 MULTILINESTRING, g3 MULTIPOLYGON)'
);
@@ -1180,7 +1180,7 @@ public function testMultiObjectSpatialDataTypes(): void {
public function testGeometryCollectionDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "g1" TEXT COLLATE NOCASE, "g2" TEXT COLLATE NOCASE )',
+ 'CREATE TABLE "t" ( "g1" TEXT COLLATE NOCASE, "g2" TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (g1 GEOMCOLLECTION, g2 GEOMETRYCOLLECTION)'
);
@@ -1201,7 +1201,7 @@ public function testGeometryCollectionDataTypes(): void {
public function testSerialDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT )',
+ 'CREATE TABLE "t" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
'CREATE TABLE t (id SERIAL)'
);
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 255adac..9f3dd36 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1152,7 +1152,7 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void {
$is_temporary = $node->get_child_node()->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL );
$element_list = $node->get_descendant_node( 'tableElementList' );
if ( true === $is_temporary || null === $element_list ) {
- $query = $this->translate( $node );
+ $query = $this->translate( $node ) . ' STRICT';
$this->execute_sqlite_query( $query );
$this->set_result_from_affected_rows();
return;
@@ -2144,7 +2144,7 @@ function ( $column ) {
$this->quote_sqlite_identifier( $new_table_name ?? $table_name )
);
$create_table_query .= implode( ",\n", $rows );
- $create_table_query .= "\n)";
+ $create_table_query .= "\n) STRICT";
return array_merge( array( $create_table_query ), $create_index_queries, $on_update_queries );
}
From 7d471e118fd586f1c988885b73bde5af78ca4a47 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 28 Jan 2025 12:52:32 +0100
Subject: [PATCH 084/124] Store last insert ID
---
tests/WP_SQLite_Driver_Tests.php | 15 +++++++++++++++
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 9 +++++++++
2 files changed, 24 insertions(+)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 8af200c..2a5e545 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -3562,4 +3562,19 @@ public static function mysqlVariablesToTest() {
array( '@@sEssIOn.sqL_moDe' ),
);
}
+
+ public function testLastInsertId(): void {
+ $this->assertQuery(
+ 'CREATE TABLE t (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ name VARCHAR(20)
+ );'
+ );
+
+ $this->assertQuery( "INSERT INTO t (name) VALUES ('a')" );
+ $this->assertEquals( 1, $this->engine->get_insert_id() );
+
+ $this->assertQuery( "INSERT INTO t (name) VALUES ('b')" );
+ $this->assertEquals( 2, $this->engine->get_insert_id() );
+ }
}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 9f3dd36..9ee2473 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1006,6 +1006,15 @@ private function execute_insert_statement( WP_Parser_Node $node ): void {
$query = implode( ' ', $parts );
$this->execute_sqlite_query( $query );
$this->set_result_from_affected_rows();
+
+ $this->last_insert_id = $this->pdo->lastInsertId();
+ if ( is_numeric( $this->last_insert_id ) ) {
+ $this->last_insert_id = (int) $this->last_insert_id;
+ }
+
+ if ( function_exists( 'apply_filters' ) ) {
+ $this->last_insert_id = apply_filters( 'sqlite_last_insert_id', $this->last_insert_id, $this->table_name );
+ }
}
private function execute_update_statement( WP_Parser_Node $node ): void {
From 92977827cb510da97a6216ccef4fdba8b78b9f9b Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 28 Jan 2025 12:56:01 +0100
Subject: [PATCH 085/124] Translate CONCAT function to concat pipes
---
tests/WP_SQLite_Driver_Translation_Tests.php | 7 +++++++
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 2 ++
2 files changed, 9 insertions(+)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 2497721..1a71de8 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -1235,6 +1235,13 @@ public function testSystemVariables(): void {
);
}
+ public function testConcatFunction(): void {
+ $this->assertQuery(
+ "SELECT ('a' || 'b' || 'c')",
+ 'SELECT CONCAT("a", "b", "c")'
+ );
+ }
+
private function assertQuery( $expected, string $query ): void {
$this->driver->query( $query );
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 9ee2473..bda271c 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1928,6 +1928,8 @@ private function translate_function_call( WP_Parser_Node $node ): string {
return sprintf( 'CAST(STRFTIME(%s, %s) AS FLOAT)', $format, $date );
}
return sprintf( 'STRFTIME(%s, %s)', $format, $date );
+ case 'CONCAT':
+ return '(' . implode( ' || ', $args ) . ')';
case 'FOUND_ROWS':
// @TODO: The following implementation with an alias assumes
// that the function is used in the SELECT field list.
From 417d37f05591c0dd1e05714ca5749a66aa60f0e9 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 28 Jan 2025 13:06:05 +0100
Subject: [PATCH 086/124] Correctly translate REPLACE statements
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index bda271c..aa91c77 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -838,7 +838,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
break;
case 'insertStatement':
$this->query_type = 'INSERT';
- $this->execute_insert_statement( $ast );
+ $this->execute_insert_or_replace_statement( $ast );
break;
case 'updateStatement':
$this->query_type = 'UPDATE';
@@ -846,9 +846,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
break;
case 'replaceStatement':
$this->query_type = 'REPLACE';
- $query = $this->translate( $ast );
- $this->execute_sqlite_query( $query );
- $this->set_result_from_affected_rows();
+ $this->execute_insert_or_replace_statement( $ast );
break;
case 'deleteStatement':
$this->query_type = 'DELETE';
@@ -994,7 +992,7 @@ private function execute_select_statement( WP_Parser_Node $node ): void {
);
}
- private function execute_insert_statement( WP_Parser_Node $node ): void {
+ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): void {
$parts = array();
foreach ( $node->get_children() as $child ) {
if ( $child instanceof WP_MySQL_Token && WP_MySQL_Lexer::IGNORE_SYMBOL === $child->id ) {
From 10dcfc133c32a8460fe3520c840ea0185c30fbde Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 28 Jan 2025 13:06:48 +0100
Subject: [PATCH 087/124] Add basic SHOW VARIABLES support
---
tests/WP_SQLite_Driver_Tests.php | 8 ++++++++
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 3 +++
2 files changed, 11 insertions(+)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 2a5e545..ca0f205 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -2793,6 +2793,14 @@ public function testShowIndex() {
);
}
+ public function testShowVarianles(): void {
+ $this->assertQuery( 'SHOW VARIABLES' );
+ $this->assertQuery( "SHOW VARIABLES LIKE 'version'" );
+ $this->assertQuery( "SHOW VARIABLES WHERE Variable_name = 'version'" );
+ $this->assertQuery( 'SHOW GLOBAL VARIABLES' );
+ $this->assertQuery( 'SHOW SESSION VARIABLES' );
+ }
+
public function testInsertOnDuplicateKeyCompositePk() {
$result = $this->assertQuery(
'CREATE TABLE wptests_term_relationships (
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index aa91c77..c809b0c 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1401,6 +1401,9 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
case WP_MySQL_Lexer::TABLES_SYMBOL:
$this->execute_show_tables_statement( $node );
break;
+ case WP_MySQL_Lexer::VARIABLES_SYMBOL:
+ $this->results = true;
+ return;
default:
throw new Exception(
sprintf(
From 8a082437c0abde47cf72477d643a0197a564c816 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 28 Jan 2025 13:07:07 +0100
Subject: [PATCH 088/124] Add support for IF NOT EXISTS when creating a table
---
tests/WP_SQLite_Driver_Tests.php | 13 +++++++++++++
.../sqlite-ast/class-wp-sqlite-driver.php | 19 ++++++++++++++++---
2 files changed, 29 insertions(+), 3 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index ca0f205..af4878c 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -3039,6 +3039,19 @@ public function testCreateTableQuery() {
);
}
+ public function testCreateTableIfNotExists(): void {
+ $this->assertQuery(
+ 'CREATE TABLE t (ID INTEGER, name TEXT)'
+ );
+ $this->assertQuery(
+ 'CREATE TABLE IF NOT EXISTS t (ID INTEGER, name TEXT)'
+ );
+ $this->assertQuery(
+ 'CREATE TABLE t (ID INTEGER, name TEXT)',
+ 'table `t` already exists'
+ );
+ }
+
public function testTranslatesComplexDelete() {
$this->sqlite->query(
"CREATE TABLE wptests_dummy (
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index c809b0c..09dd31a 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1156,8 +1156,9 @@ private function execute_delete_statement( WP_Parser_Node $node ): void {
}
private function execute_create_table_statement( WP_Parser_Node $node ): void {
- $is_temporary = $node->get_child_node()->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL );
- $element_list = $node->get_descendant_node( 'tableElementList' );
+ $subnode = $node->get_child_node();
+ $is_temporary = $subnode->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL );
+ $element_list = $subnode->get_child_node( 'tableElementList' );
if ( true === $is_temporary || null === $element_list ) {
$query = $this->translate( $node ) . ' STRICT';
$this->execute_sqlite_query( $query );
@@ -1166,9 +1167,21 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void {
}
$table_name = $this->unquote_sqlite_identifier(
- $this->translate( $node->get_descendant_node( 'tableName' ) )
+ $this->translate( $subnode->get_child_node( 'tableName' ) )
);
+ if ( $subnode->has_child_node( 'ifNotExists' ) ) {
+ $table_exists = $this->execute_sqlite_query(
+ 'SELECT 1 FROM _mysql_information_schema_tables WHERE table_schema = ? AND table_name = ?',
+ array( $this->db_name, $table_name )
+ )->fetchColumn();
+
+ if ( $table_exists ) {
+ $this->set_result_from_affected_rows( 0 );
+ return;
+ }
+ }
+
// Save information to information schema tables.
$this->information_schema_builder->record_create_table( $node );
From 56dfd8355e8f0954b4e8e6c989654528a00da6d4 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 28 Jan 2025 13:40:39 +0100
Subject: [PATCH 089/124] Improve string literal translation
---
.../sqlite-ast/class-wp-sqlite-driver.php | 88 ++++++++++++++-----
...s-wp-sqlite-pdo-user-defined-functions.php | 32 +------
2 files changed, 69 insertions(+), 51 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 09dd31a..fc73ed0 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1617,28 +1617,7 @@ private function translate( $ast ) {
case 'identifierKeyword':
return '"' . $this->translate( $ast->get_child() ) . '"';
case 'textStringLiteral':
- $token = $ast->get_child_token();
-
- // 1. Remove bounding quotes.
- $quote = $token->value[0];
- $value = substr( $token->value, 1, -1 );
-
- // 2. Unescape quotes within the string.
- $value = str_replace( $quote . $quote, $quote, $value );
- $value = str_replace( '\\' . $quote, $quote, $value );
-
- // 3. Translate datetime literals.
- // Process only strings that could possibly represent a datetime
- // literal ("YYYY-MM-DDTHH:MM:SS", "YYYY-MM-DDTHH:MM:SSZ", etc.).
- if ( strlen( $value ) >= 19 && is_numeric( $value[0] ) ) {
- $value = $this->translate_datetime_literal( $value );
- }
-
- // 4. Remove null characters.
- $value = str_replace( "\0", '', $value );
-
- // 5. Escape and add quotes.
- return "'" . str_replace( "'", "''", $value ) . "'";
+ return $this->translate_string_literal( $ast );
case 'dataType':
case 'nchar':
$child = $ast->get_child();
@@ -1779,6 +1758,71 @@ private function translate_sequence( array $nodes, string $separator = ' ' ): ?s
return implode( $separator, $parts );
}
+ private function translate_string_literal( WP_Parser_Node $node ): string {
+ $token = $node->get_child_token();
+
+ /*
+ * 1. Remove bounding quotes.
+ */
+ $quote = $token->value[0];
+ $value = substr( $token->value, 1, -1 );
+
+ /*
+ * 2. 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 in cases
+ * where only one is used. To do so correctly, we need to:
+ *
+ * 1. Skip all double backslash patterns (as "\\" resolves to "\").
+ * 2. Add an extra backslash when "\%" or "\_" follows right after.
+ *
+ * This may be related to: https://bugs.mysql.com/bug.php?id=84118
+ */
+ $value = preg_replace( '/(^|[^\\\\](?:\\\\{2}))*(\\\\[%_])/', '$1\\\\$2', $value );
+
+ /*
+ * 3. Unescape quotes within the string.
+ */
+ $value = str_replace( $quote . $quote, $quote, $value );
+
+ /*
+ * 4. Unescape C-style escape sequences.
+ *
+ * MySQL string literals are represented using C-style encoded strings,
+ * but SQLite doesn't support such escaping.
+ *
+ * @TODO: Handle NO_BACKSLASH_ESCAPES SQL mode.
+ */
+ $value = stripcslashes( $value );
+
+ /*
+ * 5. Translate datetime literals.
+ *
+ * Process only strings that could possibly represent a datetime
+ * literal ("YYYY-MM-DDTHH:MM:SS", "YYYY-MM-DDTHH:MM:SSZ", etc.).
+ */
+ if ( strlen( $value ) >= 19 && is_numeric( $value[0] ) ) {
+ $value = $this->translate_datetime_literal( $value );
+ }
+
+ /*
+ * 6. Remove null characters.
+ *
+ * SQLite doesn't support null characters in strings.
+ */
+ $value = str_replace( "\0", '', $value );
+
+ /*
+ * 7. Escape and add quotes.
+ */
+ return "'" . str_replace( "'", "''", $value ) . "'";
+ }
+
private function translate_simple_expr( WP_Parser_Node $node ): string {
$token = $node->get_child_token();
diff --git a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php
index bbf1828..d14978b 100644
--- a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php
+++ b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php
@@ -777,33 +777,7 @@ public function _helper_like_to_glob_pattern( $pattern ) {
}
/*
- * 1. 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 in cases
- * where only one is used. To do so correctly, we need to:
- *
- * 1. Skip all double backslash patterns (as "\\" resolves to "\").
- * 2. Add an extra backslash when "\%" or "\_" follows right after.
- *
- * This may be related to: https://bugs.mysql.com/bug.php?id=84118
- */
- $pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2}))*(\\\\[%_])/', '$1\\\\$2', $pattern );
-
- /*
- * 2. Unescape C-style escape sequences.
- *
- * MySQL string literals are represented using C-style encoded strings,
- * but the GLOB pattern in SQLite doesn't support such escaping.
- */
- $pattern = stripcslashes( $pattern );
-
- /*
- * 3. Escape characters that have special meaning in GLOB patterns.
+ * 1. Escape characters that have special meaning in GLOB patterns.
*
* We need to:
* 1. Escape "]" as "[]]" to avoid interpreting "[...]" as a character class.
@@ -815,7 +789,7 @@ public function _helper_like_to_glob_pattern( $pattern ) {
$pattern = str_replace( '?', '[?]', $pattern );
/*
- * 4. Convert LIKE wildcards to GLOB wildcards ("%" -> "*", "_" -> "?").
+ * 2. Convert LIKE wildcards to GLOB wildcards ("%" -> "*", "_" -> "?").
*
* We need to convert them only when they don't follow any backslashes,
* or when they follow an even number of backslashes (as "\\" is "\").
@@ -824,7 +798,7 @@ public function _helper_like_to_glob_pattern( $pattern ) {
$pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)_/', '$1?', $pattern );
/*
- * 5. Unescape LIKE escape sequences.
+ * 3. Unescape LIKE escape sequences.
*
* While in MySQL LIKE patterns, a backslash is usually used to escape
* special characters ("%", "_", and "\"), it works with all characters.
From 5db45db2429ed92412e1ede2cc396b5464c81d1d Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 28 Jan 2025 13:51:52 +0100
Subject: [PATCH 090/124] Fix backslash escaping in a test
---
tests/WP_SQLite_Driver_Tests.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index af4878c..6c5ad61 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -3215,7 +3215,7 @@ public function testTranslateLikeBinary() {
$this->assertQuery( 'INSERT INTO _tmp_table (name) VALUES (NULL);' );
$this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special%chars');" );
$this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special_chars');" );
- $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special\\chars');" );
+ $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special\\\\chars');" );
$this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('aste*risk');" );
$this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('question?mark');" );
From 39b1aba6f524fe21fd081c9c2b9ef557e99b712d Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 28 Jan 2025 13:58:25 +0100
Subject: [PATCH 091/124] Fix identifier translation, use backtics due to
issues with double quotes
See: https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted
---
tests/WP_SQLite_Driver_Translation_Tests.php | 212 +++++++++---------
.../sqlite-ast/class-wp-sqlite-driver.php | 54 +++--
2 files changed, 141 insertions(+), 125 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 1a71de8..d287c3c 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -33,174 +33,174 @@ public function testSelect(): void {
);
$this->assertQuery(
- 'SELECT * FROM "t"',
+ 'SELECT * FROM `t`',
'SELECT * FROM t'
);
$this->assertQuery(
- 'SELECT "c" FROM "t"',
+ 'SELECT `c` FROM `t`',
'SELECT c FROM t'
);
$this->assertQuery(
- 'SELECT ALL "c" FROM "t"',
+ 'SELECT ALL `c` FROM `t`',
'SELECT ALL c FROM t'
);
$this->assertQuery(
- 'SELECT DISTINCT "c" FROM "t"',
+ 'SELECT DISTINCT `c` FROM `t`',
'SELECT DISTINCT c FROM t'
);
$this->assertQuery(
- 'SELECT "c1" , "c2" FROM "t"',
+ 'SELECT `c1` , `c2` FROM `t`',
'SELECT c1, c2 FROM t'
);
$this->assertQuery(
- 'SELECT "t"."c" FROM "t"',
+ 'SELECT `t`.`c` FROM `t`',
'SELECT t.c FROM t'
);
$this->assertQuery(
- 'SELECT "c1" FROM "t" WHERE "c2" = \'abc\'',
+ 'SELECT `c1` FROM `t` WHERE `c2` = \'abc\'',
"SELECT c1 FROM t WHERE c2 = 'abc'"
);
$this->assertQuery(
- 'SELECT "c" FROM "t" GROUP BY "c"',
+ 'SELECT `c` FROM `t` GROUP BY `c`',
'SELECT c FROM t GROUP BY c'
);
$this->assertQuery(
- 'SELECT "c" FROM "t" ORDER BY "c" ASC',
+ 'SELECT `c` FROM `t` ORDER BY `c` ASC',
'SELECT c FROM t ORDER BY c ASC'
);
$this->assertQuery(
- 'SELECT "c" FROM "t" LIMIT 10',
+ 'SELECT `c` FROM `t` LIMIT 10',
'SELECT c FROM t LIMIT 10'
);
$this->assertQuery(
- 'SELECT "c" FROM "t" GROUP BY "c" HAVING COUNT ( "c" ) > 1',
+ 'SELECT `c` FROM `t` GROUP BY `c` HAVING COUNT ( `c` ) > 1',
'SELECT c FROM t GROUP BY c HAVING COUNT(c) > 1'
);
$this->assertQuery(
- 'SELECT * FROM "t1" LEFT JOIN "t2" ON "t1"."id" = "t2"."t1_id" WHERE "t1"."name" = \'abc\'',
+ 'SELECT * FROM `t1` LEFT JOIN `t2` ON `t1`.`id` = `t2`.`t1_id` WHERE `t1`.`name` = \'abc\'',
"SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id WHERE t1.name = 'abc'"
);
}
public function testInsert(): void {
$this->assertQuery(
- 'INSERT INTO "t" ( "c" ) VALUES ( 1 )',
+ 'INSERT INTO `t` ( `c` ) VALUES ( 1 )',
'INSERT INTO t (c) VALUES (1)'
);
$this->assertQuery(
- 'INSERT INTO "s"."t" ( "c" ) VALUES ( 1 )',
+ 'INSERT INTO `s`.`t` ( `c` ) VALUES ( 1 )',
'INSERT INTO s.t (c) VALUES (1)'
);
$this->assertQuery(
- 'INSERT INTO "t" ( "c1" , "c2" ) VALUES ( 1 , 2 )',
+ 'INSERT INTO `t` ( `c1` , `c2` ) VALUES ( 1 , 2 )',
'INSERT INTO t (c1, c2) VALUES (1, 2)'
);
$this->assertQuery(
- 'INSERT INTO "t" ( "c" ) VALUES ( 1 ) , ( 2 )',
+ 'INSERT INTO `t` ( `c` ) VALUES ( 1 ) , ( 2 )',
'INSERT INTO t (c) VALUES (1), (2)'
);
$this->assertQuery(
- 'INSERT INTO "t1" SELECT * FROM "t2"',
+ 'INSERT INTO `t1` SELECT * FROM `t2`',
'INSERT INTO t1 SELECT * FROM t2'
);
}
public function testReplace(): void {
$this->assertQuery(
- 'REPLACE INTO "t" ( "c" ) VALUES ( 1 )',
+ 'REPLACE INTO `t` ( `c` ) VALUES ( 1 )',
'REPLACE INTO t (c) VALUES (1)'
);
$this->assertQuery(
- 'REPLACE INTO "s"."t" ( "c" ) VALUES ( 1 )',
+ 'REPLACE INTO `s`.`t` ( `c` ) VALUES ( 1 )',
'REPLACE INTO s.t (c) VALUES (1)'
);
$this->assertQuery(
- 'REPLACE INTO "t" ( "c1" , "c2" ) VALUES ( 1 , 2 )',
+ 'REPLACE INTO `t` ( `c1` , `c2` ) VALUES ( 1 , 2 )',
'REPLACE INTO t (c1, c2) VALUES (1, 2)'
);
$this->assertQuery(
- 'REPLACE INTO "t" ( "c" ) VALUES ( 1 ) , ( 2 )',
+ 'REPLACE INTO `t` ( `c` ) VALUES ( 1 ) , ( 2 )',
'REPLACE INTO t (c) VALUES (1), (2)'
);
$this->assertQuery(
- 'REPLACE INTO "t1" SELECT * FROM "t2"',
+ 'REPLACE INTO `t1` SELECT * FROM `t2`',
'REPLACE INTO t1 SELECT * FROM t2'
);
}
public function testUpdate(): void {
$this->assertQuery(
- 'UPDATE "t" SET "c" = 1',
+ 'UPDATE `t` SET `c` = 1',
'UPDATE t SET c = 1'
);
$this->assertQuery(
- 'UPDATE "s"."t" SET "c" = 1',
+ 'UPDATE `s`.`t` SET `c` = 1',
'UPDATE s.t SET c = 1'
);
$this->assertQuery(
- 'UPDATE "t" SET "c1" = 1 , "c2" = 2',
+ 'UPDATE `t` SET `c1` = 1 , `c2` = 2',
'UPDATE t SET c1 = 1, c2 = 2'
);
$this->assertQuery(
- 'UPDATE "t" SET "c" = 1 WHERE "c" = 2',
+ 'UPDATE `t` SET `c` = 1 WHERE `c` = 2',
'UPDATE t SET c = 1 WHERE c = 2'
);
// UPDATE with LIMIT.
$this->assertQuery(
- 'UPDATE "t" SET "c" = 1 WHERE rowid IN ( SELECT rowid FROM "t" LIMIT 1 )',
+ 'UPDATE `t` SET `c` = 1 WHERE rowid IN ( SELECT rowid FROM `t` LIMIT 1 )',
'UPDATE t SET c = 1 LIMIT 1'
);
// UPDATE with ORDER BY and LIMIT.
$this->assertQuery(
- 'UPDATE "t" SET "c" = 1 WHERE rowid IN ( SELECT rowid FROM "t" ORDER BY "c" ASC LIMIT 1 )',
+ 'UPDATE `t` SET `c` = 1 WHERE rowid IN ( SELECT rowid FROM `t` ORDER BY `c` ASC LIMIT 1 )',
'UPDATE t SET c = 1 ORDER BY c ASC LIMIT 1'
);
}
public function testDelete(): void {
$this->assertQuery(
- 'DELETE FROM "t"',
+ 'DELETE FROM `t`',
'DELETE FROM t'
);
$this->assertQuery(
- 'DELETE FROM "s"."t"',
+ 'DELETE FROM `s`.`t`',
'DELETE FROM s.t'
);
$this->assertQuery(
- 'DELETE FROM "t" WHERE "c" = 1',
+ 'DELETE FROM `t` WHERE `c` = 1',
'DELETE FROM t WHERE c = 1'
);
}
public function testCreateTable(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER ) STRICT',
+ 'CREATE TABLE `t` ( `id` INTEGER ) STRICT',
'CREATE TABLE t (id INT)'
);
@@ -219,7 +219,7 @@ public function testCreateTable(): void {
public function testCreateTableWithMultipleColumns(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER, "name" TEXT COLLATE NOCASE, "score" REAL DEFAULT \'0.0\' ) STRICT',
+ 'CREATE TABLE `t` ( `id` INTEGER, `name` TEXT COLLATE NOCASE, `score` REAL DEFAULT \'0.0\' ) STRICT',
'CREATE TABLE t (id INT, name TEXT, score FLOAT DEFAULT 0.0)'
);
@@ -242,7 +242,7 @@ public function testCreateTableWithMultipleColumns(): void {
public function testCreateTableWithBasicConstraints(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
+ 'CREATE TABLE `t` ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
'CREATE TABLE t (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT)'
);
@@ -264,7 +264,7 @@ public function testCreateTableWithBasicConstraints(): void {
public function testCreateTableWithEngine(): void {
// ENGINE is not supported in SQLite, we save it in information schema.
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER ) STRICT',
+ 'CREATE TABLE `t` ( `id` INTEGER ) STRICT',
'CREATE TABLE t (id INT) ENGINE=MyISAM'
);
@@ -284,7 +284,7 @@ public function testCreateTableWithEngine(): void {
public function testCreateTableWithCollate(): void {
// COLLATE is not supported in SQLite, we save it in information schema.
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER ) STRICT',
+ 'CREATE TABLE `t` ( `id` INTEGER ) STRICT',
'CREATE TABLE t (id INT) COLLATE utf8mb4_czech_ci'
);
@@ -312,7 +312,7 @@ public function testCreateTableWithPrimaryKey(): void {
* https://www.sqlite.org/lang_createtable.html#rowids_and_the_integer_primary_key
*/
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INT NOT NULL, PRIMARY KEY ("id") ) STRICT',
+ 'CREATE TABLE `t` ( `id` INT NOT NULL, PRIMARY KEY (`id`) ) STRICT',
'CREATE TABLE t (id INT PRIMARY KEY)'
);
@@ -334,7 +334,7 @@ public function testCreateTableWithPrimaryKey(): void {
public function testCreateTableWithPrimaryKeyAndAutoincrement(): void {
// With AUTOINCREMENT, we expect "INTEGER".
$this->assertQuery(
- 'CREATE TABLE "t1" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
+ 'CREATE TABLE `t1` ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
'CREATE TABLE t1 (id INT PRIMARY KEY AUTO_INCREMENT)'
);
@@ -354,7 +354,7 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void {
// In SQLite, PRIMARY KEY must come before AUTOINCREMENT.
$this->assertQuery(
- 'CREATE TABLE "t2" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
+ 'CREATE TABLE `t2` ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
'CREATE TABLE t2 (id INT AUTO_INCREMENT PRIMARY KEY)'
);
@@ -374,7 +374,7 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void {
// In SQLite, AUTOINCREMENT cannot be specified separately from PRIMARY KEY.
$this->assertQuery(
- 'CREATE TABLE "t3" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
+ 'CREATE TABLE `t3` ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
'CREATE TABLE t3 (id INT AUTO_INCREMENT, PRIMARY KEY(id))'
);
@@ -415,9 +415,9 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void {
public function testCreateTableWithInlineUniqueIndexes(): void {
$this->assertQuery(
array(
- 'CREATE TABLE "t" ( "id" INTEGER, "name" TEXT COLLATE NOCASE ) STRICT',
- 'CREATE UNIQUE INDEX "t__id" ON "t" ("id")',
- 'CREATE UNIQUE INDEX "t__name" ON "t" ("name")',
+ 'CREATE TABLE `t` ( `id` INTEGER, `name` TEXT COLLATE NOCASE ) STRICT',
+ 'CREATE UNIQUE INDEX `t__id` ON `t` (`id`)',
+ 'CREATE UNIQUE INDEX `t__name` ON `t` (`name`)',
),
'CREATE TABLE t (id INT UNIQUE, name TEXT UNIQUE)'
);
@@ -444,9 +444,9 @@ public function testCreateTableWithInlineUniqueIndexes(): void {
public function testCreateTableWithStandaloneUniqueIndexes(): void {
$this->assertQuery(
array(
- 'CREATE TABLE "t" ( "id" INTEGER, "name" TEXT COLLATE NOCASE ) STRICT',
- 'CREATE UNIQUE INDEX "t__id" ON "t" ("id")',
- 'CREATE UNIQUE INDEX "t__name" ON "t" ("name")',
+ 'CREATE TABLE `t` ( `id` INTEGER, `name` TEXT COLLATE NOCASE ) STRICT',
+ 'CREATE UNIQUE INDEX `t__id` ON `t` (`id`)',
+ 'CREATE UNIQUE INDEX `t__name` ON `t` (`name`)',
),
'CREATE TABLE t (id INT, name VARCHAR(100), UNIQUE (id), UNIQUE (name))'
);
@@ -477,27 +477,27 @@ public function testCreateTableWithStandaloneUniqueIndexes(): void {
public function testCreateTableFromSelectQuery(): void {
// CREATE TABLE AS SELECT ...
$this->assertQuery(
- 'CREATE TABLE "t1" AS SELECT * FROM "t2" STRICT',
+ 'CREATE TABLE `t1` AS SELECT * FROM `t2` STRICT',
'CREATE TABLE t1 AS SELECT * FROM t2'
);
// CREATE TABLE SELECT ...
// The "AS" keyword is optional in MySQL, but required in SQLite.
$this->assertQuery(
- 'CREATE TABLE "t1" AS SELECT * FROM "t2" STRICT',
+ 'CREATE TABLE `t1` AS SELECT * FROM `t2` STRICT',
'CREATE TABLE t1 SELECT * FROM t2'
);
}
public function testCreateTemporaryTable(): void {
$this->assertQuery(
- 'CREATE TEMPORARY TABLE "t" ( "id" INTEGER ) STRICT',
+ 'CREATE TEMPORARY TABLE `t` ( `id` INTEGER ) STRICT',
'CREATE TEMPORARY TABLE t (id INT)'
);
// With IF NOT EXISTS.
$this->assertQuery(
- 'CREATE TEMPORARY TABLE IF NOT EXISTS "t" ( "id" INTEGER ) STRICT',
+ 'CREATE TEMPORARY TABLE IF NOT EXISTS `t` ( `id` INTEGER ) STRICT',
'CREATE TEMPORARY TABLE IF NOT EXISTS t (id INT)'
);
}
@@ -508,10 +508,10 @@ public function testAlterTableAddColumn(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER ) STRICT',
- 'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
- 'DROP TABLE "t"',
- 'ALTER TABLE "" RENAME TO "t"',
+ 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER ) STRICT',
+ 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`',
+ 'DROP TABLE `t`',
+ 'ALTER TABLE `` RENAME TO `t`',
'PRAGMA foreign_key_check',
'PRAGMA foreign_keys = ON',
),
@@ -537,10 +537,10 @@ public function testAlterTableAddColumnWithNotNull(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER NOT NULL ) STRICT',
- 'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
- 'DROP TABLE "t"',
- 'ALTER TABLE "" RENAME TO "t"',
+ 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER NOT NULL ) STRICT',
+ 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`',
+ 'DROP TABLE `t`',
+ 'ALTER TABLE `` RENAME TO `t`',
'PRAGMA foreign_key_check',
'PRAGMA foreign_keys = ON',
),
@@ -566,10 +566,10 @@ public function testAlterTableAddColumnWithDefault(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER DEFAULT \'0\' ) STRICT',
- 'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
- 'DROP TABLE "t"',
- 'ALTER TABLE "" RENAME TO "t"',
+ 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER DEFAULT \'0\' ) STRICT',
+ 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`',
+ 'DROP TABLE `t`',
+ 'ALTER TABLE `` RENAME TO `t`',
'PRAGMA foreign_key_check',
'PRAGMA foreign_keys = ON',
),
@@ -595,10 +595,10 @@ public function testAlterTableAddColumnWithNotNullAndDefault(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER NOT NULL DEFAULT \'0\' ) STRICT',
- 'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
- 'DROP TABLE "t"',
- 'ALTER TABLE "" RENAME TO "t"',
+ 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER NOT NULL DEFAULT \'0\' ) STRICT',
+ 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`',
+ 'DROP TABLE `t`',
+ 'ALTER TABLE `` RENAME TO `t`',
'PRAGMA foreign_key_check',
'PRAGMA foreign_keys = ON',
),
@@ -624,10 +624,10 @@ public function testAlterTableAddMultipleColumns(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "id" INTEGER, "a" INTEGER, "b" TEXT COLLATE NOCASE, "c" INTEGER ) STRICT',
- 'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
- 'DROP TABLE "t"',
- 'ALTER TABLE "" RENAME TO "t"',
+ 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER, `b` TEXT COLLATE NOCASE, `c` INTEGER ) STRICT',
+ 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`',
+ 'DROP TABLE `t`',
+ 'ALTER TABLE `` RENAME TO `t`',
'PRAGMA foreign_key_check',
'PRAGMA foreign_keys = ON',
),
@@ -659,10 +659,10 @@ public function testAlterTableDropColumn(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "id" INTEGER ) STRICT',
- 'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
- 'DROP TABLE "t"',
- 'ALTER TABLE "" RENAME TO "t"',
+ 'CREATE TABLE `` ( `id` INTEGER ) STRICT',
+ 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`',
+ 'DROP TABLE `t`',
+ 'ALTER TABLE `` RENAME TO `t`',
'PRAGMA foreign_key_check',
'PRAGMA foreign_keys = ON',
),
@@ -687,10 +687,10 @@ public function testAlterTableDropMultipleColumns(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "id" INTEGER ) STRICT',
- 'INSERT INTO "" ("rowid", "id") SELECT "rowid", "id" FROM "t"',
- 'DROP TABLE "t"',
- 'ALTER TABLE "" RENAME TO "t"',
+ 'CREATE TABLE `` ( `id` INTEGER ) STRICT',
+ 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`',
+ 'DROP TABLE `t`',
+ 'ALTER TABLE `` RENAME TO `t`',
'PRAGMA foreign_key_check',
'PRAGMA foreign_keys = ON',
),
@@ -719,10 +719,10 @@ public function testAlterTableAddAndDropColumns(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "b" INTEGER ) STRICT',
- 'INSERT INTO "" ("rowid") SELECT "rowid" FROM "t"',
- 'DROP TABLE "t"',
- 'ALTER TABLE "" RENAME TO "t"',
+ 'CREATE TABLE `` ( `b` INTEGER ) STRICT',
+ 'INSERT INTO `` (`rowid`) SELECT `rowid` FROM `t`',
+ 'DROP TABLE `t`',
+ 'ALTER TABLE `` RENAME TO `t`',
'PRAGMA foreign_key_check',
'PRAGMA foreign_keys = ON',
),
@@ -751,10 +751,10 @@ public function testAlterTableDropAndAddSingleColumn(): void {
array(
'PRAGMA foreign_keys',
'PRAGMA foreign_keys = OFF',
- 'CREATE TABLE "" ( "a" INTEGER ) STRICT',
- 'INSERT INTO "" ("rowid") SELECT "rowid" FROM "t"',
- 'DROP TABLE "t"',
- 'ALTER TABLE "" RENAME TO "t"',
+ 'CREATE TABLE `` ( `a` INTEGER ) STRICT',
+ 'INSERT INTO `` (`rowid`) SELECT `rowid` FROM `t`',
+ 'DROP TABLE `t`',
+ 'ALTER TABLE `` RENAME TO `t`',
'PRAGMA foreign_key_check',
'PRAGMA foreign_keys = ON',
),
@@ -779,7 +779,7 @@ public function testAlterTableDropAndAddSingleColumn(): void {
public function testBitDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "i1" INTEGER, "i2" INTEGER ) STRICT',
+ 'CREATE TABLE `t` ( `i1` INTEGER, `i2` INTEGER ) STRICT',
'CREATE TABLE t (i1 BIT, i2 BIT(10))'
);
@@ -800,7 +800,7 @@ public function testBitDataTypes(): void {
public function testBooleanDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "i1" INTEGER, "i2" INTEGER ) STRICT',
+ 'CREATE TABLE `t` ( `i1` INTEGER, `i2` INTEGER ) STRICT',
'CREATE TABLE t (i1 BOOL, i2 BOOLEAN)'
);
@@ -821,7 +821,7 @@ public function testBooleanDataTypes(): void {
public function testIntegerDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "i1" INTEGER, "i2" INTEGER, "i3" INTEGER, "i4" INTEGER, "i5" INTEGER, "i6" INTEGER ) STRICT',
+ 'CREATE TABLE `t` ( `i1` INTEGER, `i2` INTEGER, `i3` INTEGER, `i4` INTEGER, `i5` INTEGER, `i6` INTEGER ) STRICT',
'CREATE TABLE t (i1 TINYINT, i2 SMALLINT, i3 MEDIUMINT, i4 INT, i5 INTEGER, i6 BIGINT)'
);
@@ -850,7 +850,7 @@ public function testIntegerDataTypes(): void {
public function testFloatDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "f1" REAL, "f2" REAL, "f3" REAL, "f4" REAL ) STRICT',
+ 'CREATE TABLE `t` ( `f1` REAL, `f2` REAL, `f3` REAL, `f4` REAL ) STRICT',
'CREATE TABLE t (f1 FLOAT, f2 DOUBLE, f3 DOUBLE PRECISION, f4 REAL)'
);
@@ -875,7 +875,7 @@ public function testFloatDataTypes(): void {
public function testDecimalTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "f1" REAL, "f2" REAL, "f3" REAL, "f4" REAL ) STRICT',
+ 'CREATE TABLE `t` ( `f1` REAL, `f2` REAL, `f3` REAL, `f4` REAL ) STRICT',
'CREATE TABLE t (f1 DECIMAL, f2 DEC, f3 FIXED, f4 NUMERIC)'
);
@@ -900,7 +900,7 @@ public function testDecimalTypes(): void {
public function testCharDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE ) STRICT',
+ 'CREATE TABLE `t` ( `c1` TEXT COLLATE NOCASE, `c2` TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (c1 CHAR, c2 CHAR(10))'
);
@@ -921,7 +921,7 @@ public function testCharDataTypes(): void {
public function testVarcharDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE ) STRICT',
+ 'CREATE TABLE `t` ( `c1` TEXT COLLATE NOCASE, `c2` TEXT COLLATE NOCASE, `c3` TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (c1 VARCHAR(255), c2 CHAR VARYING(255), c3 CHARACTER VARYING(255))'
);
@@ -944,7 +944,7 @@ public function testVarcharDataTypes(): void {
public function testNationalCharDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE, "c4" TEXT COLLATE NOCASE ) STRICT',
+ 'CREATE TABLE `t` ( `c1` TEXT COLLATE NOCASE, `c2` TEXT COLLATE NOCASE, `c3` TEXT COLLATE NOCASE, `c4` TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (c1 NATIONAL CHAR, c2 NCHAR, c3 NATIONAL CHAR (10), c4 NCHAR(10))'
);
@@ -969,7 +969,7 @@ public function testNationalCharDataTypes(): void {
public function testNcharVarcharDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE ) STRICT',
+ 'CREATE TABLE `t` ( `c1` TEXT COLLATE NOCASE, `c2` TEXT COLLATE NOCASE, `c3` TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (c1 NCHAR VARCHAR(255), c2 NCHAR VARYING(255), c3 NVARCHAR(255))'
);
@@ -992,7 +992,7 @@ public function testNcharVarcharDataTypes(): void {
public function testNationalVarcharDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "c1" TEXT COLLATE NOCASE, "c2" TEXT COLLATE NOCASE, "c3" TEXT COLLATE NOCASE ) STRICT',
+ 'CREATE TABLE `t` ( `c1` TEXT COLLATE NOCASE, `c2` TEXT COLLATE NOCASE, `c3` TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (c1 NATIONAL VARCHAR(255), c2 NATIONAL CHAR VARYING(255), c3 NATIONAL CHARACTER VARYING(255))'
);
@@ -1015,7 +1015,7 @@ public function testNationalVarcharDataTypes(): void {
public function testTextDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "t1" TEXT COLLATE NOCASE, "t2" TEXT COLLATE NOCASE, "t3" TEXT COLLATE NOCASE, "t4" TEXT COLLATE NOCASE ) STRICT',
+ 'CREATE TABLE `t` ( `t1` TEXT COLLATE NOCASE, `t2` TEXT COLLATE NOCASE, `t3` TEXT COLLATE NOCASE, `t4` TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (t1 TINYTEXT, t2 TEXT, t3 MEDIUMTEXT, t4 LONGTEXT)'
);
@@ -1040,7 +1040,7 @@ public function testTextDataTypes(): void {
public function testEnumDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "e" TEXT COLLATE NOCASE ) STRICT',
+ 'CREATE TABLE `t` ( `e` TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (e ENUM("a", "b", "c"))'
);
@@ -1059,7 +1059,7 @@ public function testEnumDataTypes(): void {
public function testDateAndTimeDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "d" TEXT COLLATE NOCASE, "t" TEXT COLLATE NOCASE, "dt" TEXT COLLATE NOCASE, "ts" TEXT COLLATE NOCASE, "y" TEXT COLLATE NOCASE ) STRICT',
+ 'CREATE TABLE `t` ( `d` TEXT COLLATE NOCASE, `t` TEXT COLLATE NOCASE, `dt` TEXT COLLATE NOCASE, `ts` TEXT COLLATE NOCASE, `y` TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (d DATE, t TIME, dt DATETIME, ts TIMESTAMP, y YEAR)'
);
@@ -1086,7 +1086,7 @@ public function testDateAndTimeDataTypes(): void {
public function testBinaryDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "b" INTEGER, "v" BLOB ) STRICT',
+ 'CREATE TABLE `t` ( `b` INTEGER, `v` BLOB ) STRICT',
'CREATE TABLE t (b BINARY, v VARBINARY(255))'
);
@@ -1107,7 +1107,7 @@ public function testBinaryDataTypes(): void {
public function testBlobDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "b1" BLOB, "b2" BLOB, "b3" BLOB, "b4" BLOB ) STRICT',
+ 'CREATE TABLE `t` ( `b1` BLOB, `b2` BLOB, `b3` BLOB, `b4` BLOB ) STRICT',
'CREATE TABLE t (b1 TINYBLOB, b2 BLOB, b3 MEDIUMBLOB, b4 LONGBLOB)'
);
@@ -1132,7 +1132,7 @@ public function testBlobDataTypes(): void {
public function testBasicSpatialDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "g1" TEXT COLLATE NOCASE, "g2" TEXT COLLATE NOCASE, "g3" TEXT COLLATE NOCASE, "g4" TEXT COLLATE NOCASE ) STRICT',
+ 'CREATE TABLE `t` ( `g1` TEXT COLLATE NOCASE, `g2` TEXT COLLATE NOCASE, `g3` TEXT COLLATE NOCASE, `g4` TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (g1 GEOMETRY, g2 POINT, g3 LINESTRING, g4 POLYGON)'
);
@@ -1157,7 +1157,7 @@ public function testBasicSpatialDataTypes(): void {
public function testMultiObjectSpatialDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "g1" TEXT COLLATE NOCASE, "g2" TEXT COLLATE NOCASE, "g3" TEXT COLLATE NOCASE ) STRICT',
+ 'CREATE TABLE `t` ( `g1` TEXT COLLATE NOCASE, `g2` TEXT COLLATE NOCASE, `g3` TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (g1 MULTIPOINT, g2 MULTILINESTRING, g3 MULTIPOLYGON)'
);
@@ -1180,7 +1180,7 @@ public function testMultiObjectSpatialDataTypes(): void {
public function testGeometryCollectionDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "g1" TEXT COLLATE NOCASE, "g2" TEXT COLLATE NOCASE ) STRICT',
+ 'CREATE TABLE `t` ( `g1` TEXT COLLATE NOCASE, `g2` TEXT COLLATE NOCASE ) STRICT',
'CREATE TABLE t (g1 GEOMCOLLECTION, g2 GEOMETRYCOLLECTION)'
);
@@ -1201,7 +1201,7 @@ public function testGeometryCollectionDataTypes(): void {
public function testSerialDataTypes(): void {
$this->assertQuery(
- 'CREATE TABLE "t" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
+ 'CREATE TABLE `t` ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) STRICT',
'CREATE TABLE t (id SERIAL)'
);
@@ -1290,7 +1290,7 @@ function ( $query ) {
// Normalize temporary table names.
foreach ( $executed_queries as $key => $executed_query ) {
- $executed_queries[ $key ] = preg_replace( '/"_wp_sqlite_tmp_[^"]+"/', '""', $executed_query );
+ $executed_queries[ $key ] = preg_replace( '/`_wp_sqlite_tmp_[^`]+`/', '``', $executed_query );
}
$this->assertSame( $expected, $executed_queries );
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index fc73ed0..d740ecd 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1134,9 +1134,9 @@ private function execute_delete_statement( WP_Parser_Node $node ): void {
foreach ( $table_aliases as $table ) {
$this->execute_sqlite_query(
sprintf(
- 'DELETE FROM "%s" AS %s WHERE rowid IN ( %s )',
- $alias_map[ $table ],
- $table,
+ 'DELETE FROM %s AS %s WHERE rowid IN ( %s )',
+ $this->quote_sqlite_identifier( $alias_map[ $table ] ),
+ $this->quote_sqlite_identifier( $table ),
implode( ', ', array_column( $ids, "{$table}_rowid" ) )
)
);
@@ -1615,7 +1615,9 @@ private function translate( $ast ) {
case 'dotIdentifier':
return $this->translate_sequence( $ast->get_children(), '' );
case 'identifierKeyword':
- return '"' . $this->translate( $ast->get_child() ) . '"';
+ return '`' . $this->translate( $ast->get_child() ) . '`';
+ case 'pureIdentifier':
+ return $this->translate_pure_identifier( $ast );
case 'textStringLiteral':
return $this->translate_string_literal( $ast );
case 'dataType':
@@ -1712,13 +1714,6 @@ private function translate_token( WP_MySQL_Token $token ) {
switch ( $token->id ) {
case WP_MySQL_Lexer::EOF:
return null;
- case WP_MySQL_Lexer::IDENTIFIER:
- return '"' . $token->value . '"';
- case WP_MySQL_Lexer::BACK_TICK_QUOTED_ID:
- $value = substr( $token->value, 1, -1 ); // remove backticks
- $value = str_replace( '``', '`', $value ); // unescape from MySQL
- $value = str_replace( '"', '""', $value ); // escape for SQLite
- return '"' . $value . '"';
case WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL:
return 'AUTOINCREMENT';
case WP_MySQL_Lexer::BINARY_SYMBOL:
@@ -1823,13 +1818,29 @@ private function translate_string_literal( WP_Parser_Node $node ): string {
return "'" . str_replace( "'", "''", $value ) . "'";
}
+ private function translate_pure_identifier( WP_Parser_Node $node ): string {
+ $token = $node->get_child_token();
+
+ if ( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT === $token->id ) {
+ $value = substr( $token->value, 1, -1 );
+ $value = str_replace( '""', '"', $value );
+ } elseif ( WP_MySQL_Lexer::BACK_TICK_QUOTED_ID === $token->id ) {
+ $value = substr( $token->value, 1, -1 );
+ $value = str_replace( '``', '`', $value );
+ } else {
+ $value = $token->value;
+ }
+
+ return '`' . str_replace( '`', '``', $value ) . '`';
+ }
+
private function translate_simple_expr( WP_Parser_Node $node ): string {
$token = $node->get_child_token();
// Translate "VALUES(col)" to "excluded.col" in ON DUPLICATE KEY UPDATE.
if ( null !== $token && WP_MySQL_Lexer::VALUES_SYMBOL === $token->id ) {
return sprintf(
- '"excluded".%s',
+ '`excluded`.%s',
$this->translate( $node->get_child_node( 'simpleIdentifier' ) )
);
}
@@ -2187,11 +2198,16 @@ function ( $column ) {
// Prefix the original index name with the table name.
// This is to avoid conflicting index names in SQLite.
- $index_name = $table_name . '__' . $info['INDEX_NAME'];
+ $index_name = $this->quote_sqlite_identifier(
+ $table_name . '__' . $info['INDEX_NAME']
+ );
- $query = sprintf( 'CREATE %sINDEX', $is_unique ? 'UNIQUE ' : '' );
- $query .= sprintf( ' "%s"', $index_name );
- $query .= sprintf( ' ON "%s" (', $table_name );
+ $query = sprintf(
+ 'CREATE %sINDEX %s ON %s (',
+ $is_unique ? 'UNIQUE ' : '',
+ $index_name,
+ $this->quote_sqlite_identifier( $table_name )
+ );
$query .= implode(
', ',
array_map(
@@ -2361,16 +2377,16 @@ private function get_column_on_update_trigger_query( string $table, string $colu
private function unquote_sqlite_identifier( string $quoted_identifier ): string {
$first_byte = $quoted_identifier[0] ?? null;
- if ( '"' === $first_byte ) {
+ if ( '"' === $first_byte || '`' === $first_byte ) {
$unquoted = substr( $quoted_identifier, 1, -1 );
} else {
$unquoted = $quoted_identifier;
}
- return str_replace( '""', '"', $unquoted );
+ return str_replace( $first_byte . $first_byte, $first_byte, $unquoted );
}
private function quote_sqlite_identifier( string $unquoted_identifier ): string {
- return '"' . str_replace( '"', '""', $unquoted_identifier ) . '"';
+ return '`' . str_replace( '`', '``', $unquoted_identifier ) . '`';
}
private function quote_mysql_identifier( string $unquoted_identifier ): string {
From 118c57ac0627905e067008d77032fb1126043f76 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 28 Jan 2025 15:21:02 +0100
Subject: [PATCH 092/124] Do not execute statements when there are no IDs to
delete
---
.../sqlite-ast/class-wp-sqlite-driver.php | 24 ++++++++++---------
1 file changed, 13 insertions(+), 11 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index d740ecd..6bf9913 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1131,17 +1131,19 @@ private function execute_delete_statement( WP_Parser_Node $node ): void {
// 4. Execute DELETE statements for each table.
$rows = 0;
- foreach ( $table_aliases as $table ) {
- $this->execute_sqlite_query(
- sprintf(
- 'DELETE FROM %s AS %s WHERE rowid IN ( %s )',
- $this->quote_sqlite_identifier( $alias_map[ $table ] ),
- $this->quote_sqlite_identifier( $table ),
- implode( ', ', array_column( $ids, "{$table}_rowid" ) )
- )
- );
- $this->set_result_from_affected_rows();
- $rows += $this->affected_rows;
+ if ( count( $ids ) > 0 ) {
+ foreach ( $table_aliases as $table ) {
+ $this->execute_sqlite_query(
+ sprintf(
+ 'DELETE FROM %s AS %s WHERE rowid IN ( %s )',
+ $this->quote_sqlite_identifier( $alias_map[ $table ] ),
+ $this->quote_sqlite_identifier( $table ),
+ implode( ', ', array_column( $ids, "{$table}_rowid" ) )
+ )
+ );
+ $this->set_result_from_affected_rows();
+ $rows += $this->affected_rows;
+ }
}
$this->set_result_from_affected_rows( $rows );
From b78d0ad3e3b01be36e8b72bed8b4a9ed9192831a Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 28 Jan 2025 15:21:19 +0100
Subject: [PATCH 093/124] Add basic support for CHAR_LENGTH
---
tests/WP_SQLite_Driver_Tests.php | 59 +++++++++++++++++++
.../sqlite-ast/class-wp-sqlite-driver.php | 3 +
2 files changed, 62 insertions(+)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 6c5ad61..67957d7 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -3598,4 +3598,63 @@ public function testLastInsertId(): void {
$this->assertQuery( "INSERT INTO t (name) VALUES ('b')" );
$this->assertEquals( 2, $this->engine->get_insert_id() );
}
+
+ public function testCharLength(): void {
+ $this->assertQuery(
+ 'CREATE TABLE t (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ name VARCHAR(20)
+ );'
+ );
+
+ $this->assertQuery( "INSERT INTO t (name) VALUES ('a')" );
+ $this->assertQuery( "INSERT INTO t (name) VALUES ('ab')" );
+ $this->assertQuery( "INSERT INTO t (name) VALUES ('abc')" );
+
+ $this->assertQuery( 'SELECT CHAR_LENGTH(name) AS len FROM t' );
+ $this->assertEquals(
+ array(
+ (object) array( 'len' => '1' ),
+ (object) array( 'len' => '2' ),
+ (object) array( 'len' => '3' ),
+ ),
+ $this->engine->get_query_results()
+ );
+ }
+
+ public function _testAsdf() {
+ $this->assertQuery(
+ 'CREATE TABLE t (id INT)'
+ );
+ $this->assertQuery(
+ 'INSERT INTO t (id) VALUES (1)'
+ );
+ $this->assertQuery(
+ 'SELECT non_existent_column FROM t LIMIT 0'
+ );
+ var_dump( $this->engine->executed_sqlite_queries );
+ var_dump( $this->engine->get_error_message() );
+ $this->assertCount( 1, $this->engine->get_query_results() );
+ }
+
+ public function _testXyz() {
+ $this->assertQuery(
+ "INSERT INTO wp_actionscheduler_actions ( `hook`, `status`, `scheduled_date_gmt`, `scheduled_date_local`, `schedule`, `group_id`, `priority`, `args` ) SELECT 'action_scheduler/migration_hook', 'pending', '2025-01-28 15:14:01', '2025-01-28 15:14:01', 'O:30:\"ActionScheduler_SimpleSchedule\":2:{s:22:\"\0*\0scheduled_timestamp\";i:1738077241;s:41:\"\0ActionScheduler_SimpleSchedule\0timestamp\";i:1738077241;}', 2, 10, '[]' FROM DUAL WHERE ( SELECT NULL FROM DUAL ) IS NULL"
+ );
+ }
+
+ public function testAaa() {
+ $this->assertQuery(
+ '
+ CREATE TABLE t (
+ id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ name varchar(90) NOT NULL,
+ type varchar(90) NOT NULL DEFAULT \'default\',
+ description varchar(250) NOT NULL DEFAULT \'\',
+ created_at timestamp NULL,
+ updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+ )
+ '
+ );
+ }
}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 6bf9913..e2da9e0 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1999,6 +1999,9 @@ private function translate_function_call( WP_Parser_Node $node ): string {
return sprintf( 'CAST(STRFTIME(%s, %s) AS FLOAT)', $format, $date );
}
return sprintf( 'STRFTIME(%s, %s)', $format, $date );
+ case 'CHAR_LENGTH':
+ // @TODO LENGTH and CHAR_LENGTH aren't always the same in MySQL for utf8 characters.
+ return 'LENGTH(' . $args[0] . ')';
case 'CONCAT':
return '(' . implode( ' || ', $args ) . ')';
case 'FOUND_ROWS':
From 3cbb64719073fbe45b9b3db6bf52280ca9409665 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 29 Jan 2025 10:24:05 +0100
Subject: [PATCH 094/124] Handle null characters in strings
---
tests/WP_SQLite_Driver_Tests.php | 36 +++++--------------
.../sqlite-ast/class-wp-sqlite-driver.php | 34 +++++++++++++-----
2 files changed, 35 insertions(+), 35 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 67957d7..6631d07 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -3622,39 +3622,21 @@ public function testCharLength(): void {
);
}
- public function _testAsdf() {
+ public function testNullCharactersInStrings(): void {
$this->assertQuery(
- 'CREATE TABLE t (id INT)'
+ 'CREATE TABLE t (id INT, name VARCHAR(20))'
);
$this->assertQuery(
- 'INSERT INTO t (id) VALUES (1)'
+ "INSERT INTO t (name) VALUES ('a\0b')"
);
$this->assertQuery(
- 'SELECT non_existent_column FROM t LIMIT 0'
+ 'SELECT name FROM t'
);
- var_dump( $this->engine->executed_sqlite_queries );
- var_dump( $this->engine->get_error_message() );
- $this->assertCount( 1, $this->engine->get_query_results() );
- }
-
- public function _testXyz() {
- $this->assertQuery(
- "INSERT INTO wp_actionscheduler_actions ( `hook`, `status`, `scheduled_date_gmt`, `scheduled_date_local`, `schedule`, `group_id`, `priority`, `args` ) SELECT 'action_scheduler/migration_hook', 'pending', '2025-01-28 15:14:01', '2025-01-28 15:14:01', 'O:30:\"ActionScheduler_SimpleSchedule\":2:{s:22:\"\0*\0scheduled_timestamp\";i:1738077241;s:41:\"\0ActionScheduler_SimpleSchedule\0timestamp\";i:1738077241;}', 2, 10, '[]' FROM DUAL WHERE ( SELECT NULL FROM DUAL ) IS NULL"
- );
- }
-
- public function testAaa() {
- $this->assertQuery(
- '
- CREATE TABLE t (
- id int(11) unsigned NOT NULL AUTO_INCREMENT,
- name varchar(90) NOT NULL,
- type varchar(90) NOT NULL DEFAULT \'default\',
- description varchar(250) NOT NULL DEFAULT \'\',
- created_at timestamp NULL,
- updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- )
- '
+ $this->assertEquals(
+ array(
+ (object) array( 'name' => "a\0b" ),
+ ),
+ $this->engine->get_query_results()
);
}
}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index e2da9e0..40e9048 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1808,16 +1808,34 @@ private function translate_string_literal( WP_Parser_Node $node ): string {
}
/*
- * 6. Remove null characters.
+ * 6. Handle null characters.
*
- * SQLite doesn't support null characters in strings.
- */
- $value = str_replace( "\0", '', $value );
-
- /*
- * 7. Escape and add quotes.
+ * SQLite doesn't fully support null characters (\u0000) in strings.
+ * However, it can store them and read them, with some limitations.
+ *
+ * In PHP, null bytes are often produced by the serialize() function.
+ * Removing them would damage the serialized data.
+ *
+ * There is no way to store null bytes using a string literal, so we
+ * need to split the string and concatenate null bytes with its parts.
+ * This will convert literals will null bytes to expressions.
+ *
+ * Alternatively, we could replace string literals with parameters and
+ * pass them using prepared statements. However, that's not universally
+ * applicable for all string literals (e.g., in default column values).
+ *
+ * See:
+ * https://www.sqlite.org/nulinstr.html
*/
- return "'" . str_replace( "'", "''", $value ) . "'";
+ $parts = array();
+ foreach ( explode( "\0", $value ) as $segment ) {
+ // Escape and quote each segment.
+ $parts[] = "'" . str_replace( "'", "''", $segment ) . "'";
+ }
+ if ( count( $parts ) > 1 ) {
+ return '(' . implode( ' || CHAR(0) || ', $parts ) . ')';
+ }
+ return $parts[0];
}
private function translate_pure_identifier( WP_Parser_Node $node ): string {
From 50d0bd2f4153b2cec91f4c14709f21a58226cbae Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 29 Jan 2025 14:48:03 +0100
Subject: [PATCH 095/124] Fix column default value handling
---
tests/WP_SQLite_Driver_Tests.php | 36 ++++++++++++--
.../sqlite-ast/class-wp-sqlite-driver.php | 36 ++++++++++++--
...s-wp-sqlite-information-schema-builder.php | 48 +++++++++++++++----
3 files changed, 101 insertions(+), 19 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 6631d07..547334f 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -476,7 +476,7 @@ public function testShowCreateTableWithCorrectDefaultValues() {
'CREATE TABLE `_tmp__table` (
`ID` bigint NOT NULL AUTO_INCREMENT,
`default_empty_string` varchar(255) DEFAULT \'\',
- `null_no_default` varchar(255),
+ `null_no_default` varchar(255) DEFAULT NULL,
PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci',
$results[0]->{'Create Table'}
@@ -1954,7 +1954,7 @@ public function testAlterTableModifyColumnWithHyphens() {
'Type' => 'text',
'Null' => 'YES',
'Key' => '',
- 'Default' => 'NULL',
+ 'Default' => null,
'Extra' => '',
),
),
@@ -3405,7 +3405,7 @@ public function testUniqueConstraints() {
public function testDefaultNullValue() {
$this->assertQuery(
'CREATE TABLE _tmp_table (
- name varchar(20) NOT NULL default NULL,
+ name varchar(20) default NULL,
no_default varchar(20) NOT NULL
);'
);
@@ -3418,9 +3418,9 @@ public function testDefaultNullValue() {
(object) array(
'Field' => 'name',
'Type' => 'varchar(20)',
- 'Null' => 'NO',
+ 'Null' => 'YES',
'Key' => '',
- 'Default' => 'NULL',
+ 'Default' => null,
'Extra' => '',
),
(object) array(
@@ -3639,4 +3639,30 @@ public function testNullCharactersInStrings(): void {
$this->engine->get_query_results()
);
}
+
+ public function testColumnDefaults(): void {
+ $this->assertQuery(
+ "
+ CREATE TABLE t (
+ name varchar(255) DEFAULT 'CURRENT_TIMESTAMP',
+ type varchar(255) NOT NULL DEFAULT 'DEFAULT',
+ description varchar(250) NOT NULL DEFAULT '',
+ created_at timestamp DEFAULT CURRENT_TIMESTAMP,
+ updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+ )
+ "
+ );
+
+ $result = $this->assertQuery( 'SHOW CREATE TABLE t' );
+ $this->assertEquals(
+ "CREATE TABLE `t` (\n"
+ . " `name` varchar(255) DEFAULT 'CURRENT_TIMESTAMP',\n"
+ . " `type` varchar(255) NOT NULL DEFAULT 'DEFAULT',\n"
+ . " `description` varchar(250) NOT NULL DEFAULT '',\n"
+ . " `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,\n"
+ . " `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n"
+ . ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci',
+ $result[0]->{'Create Table'}
+ );
+ }
}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 40e9048..c69e771 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -2181,8 +2181,18 @@ private function get_sqlite_create_table_statement( string $table_name, ?string
$query .= ' PRIMARY KEY AUTOINCREMENT';
}
if ( null !== $column['COLUMN_DEFAULT'] ) {
- // @TODO: Correctly quote based on the data type.
- $query .= ' DEFAULT ' . $this->pdo->quote( $column['COLUMN_DEFAULT'] );
+ // @TODO: Handle defaults with expression values (DEFAULT_GENERATED).
+
+ // Handle DEFAULT CURRENT_TIMESTAMP. This works only with timestamp
+ // and datetime columns. For other column types, it's just a string.
+ if (
+ 'CURRENT_TIMESTAMP' === $column['COLUMN_DEFAULT']
+ && ( 'timestamp' === $column['DATA_TYPE'] || 'datetime' === $column['DATA_TYPE'] )
+ ) {
+ $query .= ' DEFAULT CURRENT_TIMESTAMP';
+ } else {
+ $query .= ' DEFAULT ' . $this->pdo->quote( $column['COLUMN_DEFAULT'] );
+ }
}
$rows[] = $query;
@@ -2300,14 +2310,32 @@ private function get_mysql_create_table_statement( string $table_name ): ?string
$sql .= ' ' . $column['COLUMN_TYPE'];
if ( 'NO' === $column['IS_NULLABLE'] ) {
$sql .= ' NOT NULL';
+ } elseif ( 'timestamp' === $column['COLUMN_TYPE'] ) {
+ // Nullable "timestamp" columns dump NULL explicitly.
+ $sql .= ' NULL';
}
if ( 'auto_increment' === $column['EXTRA'] ) {
$sql .= ' AUTO_INCREMENT';
}
- if ( null !== $column['COLUMN_DEFAULT'] ) {
- // @TODO: Correctly quote based on the data type.
+
+ // Handle DEFAULT CURRENT_TIMESTAMP. This works only with timestamp
+ // and datetime columns. For other column types, it's just a string.
+ if (
+ 'CURRENT_TIMESTAMP' === $column['COLUMN_DEFAULT']
+ && ( 'timestamp' === $column['DATA_TYPE'] || 'datetime' === $column['DATA_TYPE'] )
+ ) {
+ $sql .= ' DEFAULT CURRENT_TIMESTAMP';
+ } elseif ( null !== $column['COLUMN_DEFAULT'] ) {
$sql .= ' DEFAULT ' . $this->pdo->quote( $column['COLUMN_DEFAULT'] );
+ } elseif ( 'YES' === $column['IS_NULLABLE'] ) {
+ $sql .= ' DEFAULT NULL';
}
+
+ // Handle ON UPDATE CURRENT_TIMESTAMP.
+ if ( str_contains( $column['EXTRA'], 'on update CURRENT_TIMESTAMP' ) ) {
+ $sql .= ' ON UPDATE CURRENT_TIMESTAMP';
+ }
+
$rows[] = $sql;
}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index 9022fe9..271cf55 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -905,14 +905,31 @@ private function get_table_collation( WP_Parser_Node $node ): string {
}
private function get_column_default( WP_Parser_Node $node ): ?string {
+ $default_attr = null;
foreach ( $node->get_descendant_nodes( 'columnAttribute' ) as $attr ) {
if ( $attr->has_child_token( WP_MySQL_Lexer::DEFAULT_SYMBOL ) ) {
- // @TODO: MySQL seems to normalize default values for numeric
- // columns, such as 1.0 to 1, 1e3 to 1000, etc.
- return substr( $this->get_value( $attr ), strlen( 'DEFAULT' ) );
+ $default_attr = $attr;
}
}
- return null;
+
+ if ( null === $default_attr ) {
+ return null;
+ }
+
+ if ( $default_attr->has_child_token( WP_MySQL_Lexer::NOW_SYMBOL ) ) {
+ return 'CURRENT_TIMESTAMP';
+ }
+
+ if (
+ $default_attr->has_child_node( 'signedLiteral' )
+ && null !== $default_attr->get_descendant_node( 'nullLiteral' )
+ ) {
+ return null;
+ }
+
+ // @TODO: MySQL seems to normalize default values for numeric
+ // columns, such as 1.0 to 1, 1e3 to 1000, etc.
+ return $this->get_value( $default_attr->get_child_node() );
}
private function get_column_nullable( WP_Parser_Node $node ): string {
@@ -967,7 +984,8 @@ private function get_column_key( WP_Parser_Node $column_node ): string {
}
private function get_column_extra( WP_Parser_Node $node ): string {
- $extra = '';
+ $extras = array();
+ $attributes = $node->get_descendant_nodes( 'columnAttribute' );
// SERIAL
$data_type = $node->get_descendant_node( 'dataType' );
@@ -975,7 +993,17 @@ private function get_column_extra( WP_Parser_Node $node ): string {
return 'auto_increment';
}
- foreach ( $node->get_descendant_nodes( 'columnAttribute' ) as $attr ) {
+ // Check whether DEFAULT value is an expression.
+ foreach ( $attributes as $attr ) {
+ if (
+ $attr->has_child_token( WP_MySQL_Lexer::DEFAULT_SYMBOL )
+ && $attr->has_child_node( 'exprWithParentheses' )
+ ) {
+ $extras[] = 'DEFAULT_GENERATED';
+ }
+ }
+
+ foreach ( $attributes as $attr ) {
if ( $attr->has_child_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) ) {
return 'auto_increment';
}
@@ -983,16 +1011,16 @@ private function get_column_extra( WP_Parser_Node $node ): string {
$attr->has_child_token( WP_MySQL_Lexer::ON_SYMBOL )
&& $attr->has_child_token( WP_MySQL_Lexer::UPDATE_SYMBOL )
) {
- return 'on update CURRENT_TIMESTAMP';
+ $extras[] = 'on update CURRENT_TIMESTAMP';
}
}
if ( $node->get_descendant_token( WP_MySQL_Lexer::VIRTUAL_SYMBOL ) ) {
- $extra = 'VIRTUAL GENERATED';
+ $extras[] = 'VIRTUAL GENERATED';
} elseif ( $node->get_descendant_token( WP_MySQL_Lexer::STORED_SYMBOL ) ) {
- $extra = 'STORED GENERATED';
+ $extras[] = 'STORED GENERATED';
}
- return $extra;
+ return implode( ' ', $extras );
}
private function get_column_comment( WP_Parser_Node $node ): string {
From bbe647075536f57e428b6581ee143baf863ef242 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 29 Jan 2025 15:21:13 +0100
Subject: [PATCH 096/124] Display info when the AST driver is used
---
admin-page.php | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/admin-page.php b/admin-page.php
index 6a44c0e..24cfcc4 100644
--- a/admin-page.php
+++ b/admin-page.php
@@ -130,7 +130,8 @@ function sqlite_plugin_adminbar_item( $admin_bar ) {
global $wpdb;
if ( defined( 'SQLITE_DB_DROPIN_VERSION' ) && defined( 'DB_ENGINE' ) && 'sqlite' === DB_ENGINE ) {
- $title = '' . __( 'Database: SQLite', 'sqlite-database-integration' ) . '';
+ $suffix = defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ? ' (AST)' : '';
+ $title = '' . __( 'Database: SQLite', 'sqlite-database-integration' ) . $suffix . '';
} elseif ( stripos( $wpdb->db_server_info(), 'maria' ) !== false ) {
$title = '' . __( 'Database: MariaDB', 'sqlite-database-integration' ) . '';
} else {
From 069f1ef306c8d2848ca196d2806ac01f81573884 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 30 Jan 2025 13:45:21 +0100
Subject: [PATCH 097/124] Add a test for the reason to use backtick identifier
escaping
---
tests/WP_SQLite_Driver_Tests.php | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 547334f..38df86d 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -3665,4 +3665,23 @@ public function testColumnDefaults(): void {
$result[0]->{'Create Table'}
);
}
+
+ public function testSelectNonExistentColumn(): void {
+ $this->assertQuery(
+ 'CREATE TABLE t (id INT)'
+ );
+
+ /*
+ * Here, we're basically testing that identifiers are escaped using
+ * backticks instead of double quotes. In SQLite, double quotes may
+ * fallback to a string literal and thus produce no error.
+ *
+ * See:
+ * https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted
+ */
+ $this->assertQuery(
+ 'SELECT non_existent_column FROM t LIMIT 0',
+ 'no such column: non_existent_column'
+ );
+ }
}
From 487bf5467ff36e69b804ad0642ad7b8797013c55 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Mon, 3 Feb 2025 15:02:57 +0100
Subject: [PATCH 098/124] Use PDO to load SQLite version, improve docs
---
.../sqlite-ast/class-wp-sqlite-driver.php | 41 ++++++++++++-------
wp-includes/sqlite/class-wp-sqlite-db.php | 6 +--
.../sqlite/class-wp-sqlite-translator.php | 11 ++++-
3 files changed, 40 insertions(+), 18 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index c69e771..1e90029 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -193,6 +193,22 @@ class WP_SQLite_Driver {
'%y' => '%y',
);
+ /**
+ * The SQLite version.
+ *
+ * This is a mysqli-like property that is needed to avoid a PHP warning in
+ * the WordPress health info. The "WP_Debug_Data::get_wp_database()" method
+ * calls "$wpdb->dbh->client_info" - a mysqli-specific abstraction leak.
+ *
+ * @TODO: This should be fixed in WordPress core.
+ *
+ * See:
+ * https://github.com/WordPress/wordpress-develop/blob/bcdca3f9925f1d3eca7b78d231837c0caf0c8c24/src/wp-admin/includes/class-wp-debug-data.php#L1579
+ *
+ * @var string
+ */
+ public $client_info;
+
/**
* @var WP_Parser_Grammar
*/
@@ -214,15 +230,6 @@ class WP_SQLite_Driver {
*/
private $pdo;
- /**
- * The database version.
- *
- * This is used here to avoid PHP warnings in the health screen.
- *
- * @var string
- */
- public $client_info = '';
-
/**
* Last executed MySQL query.
*
@@ -434,11 +441,8 @@ public function __construct( string $db_name, ?PDO $pdo = null ) {
self::$grammar = new WP_Parser_Grammar( require self::GRAMMAR_PATH );
}
- // Fixes a warning in the site-health screen.
- // @TODO: It is not clear how this fixes a warning or if it serves a
- // different purpose. We should also make this use PDO and
- // consider providing that information lazily via a getter.
- $this->client_info = SQLite3::version()['versionString'];
+ // Load SQLite version to a property used by WordPress health info.
+ $this->client_info = $this->get_sqlite_version();
$this->pdo->query( 'PRAGMA foreign_keys = ON' );
$this->pdo->query( 'PRAGMA encoding="UTF-8";' );
@@ -458,6 +462,15 @@ public function get_pdo() {
return $this->pdo;
}
+ /**
+ * Get the version of the SQLite engine.
+ *
+ * @return string SQLite engine version as a string.
+ */
+ public function get_sqlite_version(): string {
+ return $this->pdo->query( 'SELECT SQLITE_VERSION()' )->fetchColumn();
+ }
+
/**
* Method to return inserted row id.
*/
diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php
index bb993db..77567eb 100644
--- a/wp-includes/sqlite/class-wp-sqlite-db.php
+++ b/wp-includes/sqlite/class-wp-sqlite-db.php
@@ -366,11 +366,11 @@ public function db_version() {
}
/**
- * Retrieves full database server information.
+ * Returns the version of the SQLite engine.
*
- * @return string|false Server info on success, false on failure.
+ * @return string SQLite engine version as a string.
*/
public function db_server_info() {
- return SQLite3::version()['versionString'];
+ return $this->dbh->get_sqlite_version();
}
}
diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php
index 6892de0..c353668 100644
--- a/wp-includes/sqlite/class-wp-sqlite-translator.php
+++ b/wp-includes/sqlite/class-wp-sqlite-translator.php
@@ -410,7 +410,7 @@ public function __construct( $pdo = null ) {
$this->pdo = $pdo;
// Fixes a warning in the site-health screen.
- $this->client_info = SQLite3::version()['versionString'];
+ $this->client_info = $this->get_sqlite_version();
register_shutdown_function( array( $this, '__destruct' ) );
@@ -474,6 +474,15 @@ public function get_pdo() {
return $this->pdo;
}
+ /**
+ * Get the version of the SQLite engine.
+ *
+ * @return string SQLite engine version as a string.
+ */
+ public function get_sqlite_version(): string {
+ return $this->pdo->query( 'SELECT SQLITE_VERSION()' )->fetchColumn();
+ }
+
/**
* Method to return inserted row id.
*/
From c63607ee08974e174ef45ff50984540ec0bb8072 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Mon, 3 Feb 2025 15:28:50 +0100
Subject: [PATCH 099/124] Remove all remaining usages of SQLite 3
---
activate.php | 4 ++--
admin-notices.php | 6 +++---
admin-page.php | 6 +-----
health-check.php | 2 +-
4 files changed, 7 insertions(+), 11 deletions(-)
diff --git a/activate.php b/activate.php
index e91e181..197f162 100644
--- a/activate.php
+++ b/activate.php
@@ -75,8 +75,8 @@ function ( $result ) {
* When the plugin gets merged in wp-core, this is not to be ported.
*/
function sqlite_plugin_copy_db_file() {
- // Bail early if the SQLite3 class does not exist.
- if ( ! class_exists( 'SQLite3' ) ) {
+ // Bail early if the PDO SQLite extension is not loaded.
+ if ( ! extension_loaded( 'pdo_sqlite' ) ) {
return;
}
diff --git a/admin-notices.php b/admin-notices.php
index 8eaf253..a455cc8 100644
--- a/admin-notices.php
+++ b/admin-notices.php
@@ -19,11 +19,11 @@ function sqlite_plugin_admin_notice() {
return;
}
- // If SQLite is not detected, bail early.
- if ( ! class_exists( 'SQLite3' ) ) {
+ // If PDO SQLite is not loaded, bail early.
+ if ( ! extension_loaded( 'pdo_sqlite' ) ) {
printf(
'',
- esc_html__( 'The SQLite Integration plugin is active, but the SQLite3 class is missing from your server. Please make sure that SQLite is enabled in your PHP installation.', 'sqlite-database-integration' )
+ esc_html__( 'The SQLite Integration plugin is active, but the PDO SQLite extension is missing from your server. Please make sure that PDO SQLite is enabled in your PHP installation.', 'sqlite-database-integration' )
);
return;
}
diff --git a/admin-page.php b/admin-page.php
index 24cfcc4..c89f4dd 100644
--- a/admin-page.php
+++ b/admin-page.php
@@ -47,11 +47,7 @@ function sqlite_integration_admin_screen() {
?>
-
-
-
+
diff --git a/health-check.php b/health-check.php
index 6c64550..cf69af3 100644
--- a/health-check.php
+++ b/health-check.php
@@ -31,7 +31,7 @@ function sqlite_plugin_filter_debug_data( $info ) {
if ( 'sqlite' === $db_engine ) {
$info['wp-database']['fields']['database_version'] = array(
'label' => __( 'SQLite version', 'sqlite-database-integration' ),
- 'value' => class_exists( 'SQLite3' ) ? SQLite3::version()['versionString'] : null,
+ 'value' => $info['wp-database']['fields']['server_version'] ?? null,
);
$info['wp-database']['fields']['database_file'] = array(
From 39ac1733a46baa062e4c7ffbcb0f30dd65954a84 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Mon, 3 Feb 2025 15:29:57 +0100
Subject: [PATCH 100/124] Declare PDO and PDO SQLite requirements in
composer.json
---
composer.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/composer.json b/composer.json
index dce2c54..975debc 100644
--- a/composer.json
+++ b/composer.json
@@ -8,7 +8,9 @@
"issues": "https://github.com/wordpress/sqlite-database-integration/issues"
},
"require": {
- "php": ">=7.0"
+ "php": ">=7.0",
+ "ext-pdo": "*",
+ "ext-pdo_sqlite": "*"
},
"require-dev": {
"ext-mbstring": "*",
From d2be9d8dd233e5ab2e860663d00c730ea13b4289 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Mon, 3 Feb 2025 15:44:36 +0100
Subject: [PATCH 101/124] Use getters instead of public properties
---
tests/WP_SQLite_Driver_Translation_Tests.php | 4 ++--
tests/tools/dump-sqlite-query.php | 2 +-
.../sqlite-ast/class-wp-sqlite-driver.php | 24 ++++++++++++++++---
3 files changed, 24 insertions(+), 6 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index d287c3c..45ffbfc 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -1255,7 +1255,7 @@ private function assertQuery( $expected, string $query ): void {
);
}
- $executed_queries = array_column( $this->driver->executed_sqlite_queries, 'sql' );
+ $executed_queries = array_column( $this->driver->get_executed_sqlite_queries(), 'sql' );
// Remove BEGIN and COMMIT/ROLLBACK queries.
if ( count( $executed_queries ) > 2 ) {
@@ -1299,7 +1299,7 @@ function ( $query ) {
private function assertExecutedInformationSchemaQueries( array $expected ): void {
// Collect and normalize "information_schema" queries.
$queries = array();
- foreach ( $this->driver->executed_sqlite_queries as $query ) {
+ foreach ( $this->driver->get_executed_sqlite_queries() as $query ) {
if ( ! str_contains( $query['sql'], '_mysql_information_schema_' ) ) {
continue;
}
diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php
index 56718f0..1b6e86b 100644
--- a/tests/tools/dump-sqlite-query.php
+++ b/tests/tools/dump-sqlite-query.php
@@ -20,7 +20,7 @@
$driver->query( $query );
-$executed_queries = $driver->executed_sqlite_queries;
+$executed_queries = $driver->get_executed_sqlite_queries();
if ( count( $executed_queries ) > 2 ) {
// Remove BEGIN and COMMIT/ROLLBACK queries.
$executed_queries = array_values( array_slice( $executed_queries, 1, -1, true ) );
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 1e90029..caf7d56 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -235,14 +235,14 @@ class WP_SQLite_Driver {
*
* @var string
*/
- public $mysql_query;
+ private $mysql_query;
/**
* A list of executed SQLite queries.
*
* @var array
*/
- public $executed_sqlite_queries = array();
+ private $executed_sqlite_queries = array();
/**
* The affected table name.
@@ -279,7 +279,7 @@ class WP_SQLite_Driver {
*
* @var boolean
*/
- public $is_error = false;
+ private $is_error = false;
/**
* Class variable to store the file name and function to cause error.
@@ -471,6 +471,24 @@ public function get_sqlite_version(): string {
return $this->pdo->query( 'SELECT SQLITE_VERSION()' )->fetchColumn();
}
+ /**
+ * Get the last executed MySQL query.
+ *
+ * @return string|null
+ */
+ public function get_mysql_query(): ?string {
+ return $this->mysql_query;
+ }
+
+ /**
+ * Get all SQLite queries executed for the last MySQL query.
+ *
+ * @return array
+ */
+ public function get_executed_sqlite_queries(): array {
+ return $this->executed_sqlite_queries;
+ }
+
/**
* Method to return inserted row id.
*/
From 8b74f335f0359142f7d25f63b3ba7c0cb1e1583f Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Mon, 3 Feb 2025 15:44:47 +0100
Subject: [PATCH 102/124] Fix query dumping script
---
tests/tools/dump-sqlite-query.php | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php
index 1b6e86b..d222ae8 100644
--- a/tests/tools/dump-sqlite-query.php
+++ b/tests/tools/dump-sqlite-query.php
@@ -8,13 +8,10 @@
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php';
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php';
require_once __DIR__ . '/../../wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php';
-require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-expression.php';
+require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
-require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-token-factory.php';
-require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-token.php';
-require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-query-builder.php';
-$driver = new WP_SQLite_Driver( new PDO( 'sqlite::memory:' ) );
+$driver = new WP_SQLite_Driver( 'wp', new PDO( 'sqlite::memory:' ) );
$query = "SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id WHERE t1.name = 'abc'";
From 625ca4ae2e6c44ec8c9da2c8d9721f60490b9c4e Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 4 Feb 2025 09:43:25 +0100
Subject: [PATCH 103/124] Simplify SQLite lock wait logic
---
.../sqlite-ast/class-wp-sqlite-driver.php | 58 ++++++-------------
1 file changed, 17 insertions(+), 41 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index caf7d56..cc63924 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -8,8 +8,10 @@
class WP_SQLite_Driver {
const GRAMMAR_PATH = __DIR__ . '/../../wp-includes/mysql/mysql-grammar.php';
- const SQLITE_BUSY = 5;
- const SQLITE_LOCKED = 6;
+ /**
+ * The default timeout in seconds for SQLite to wait for a writable lock.
+ */
+ const DEFAULT_TIMEOUT = 10;
/**
* An identifier prefix for internal objects.
@@ -387,38 +389,22 @@ public function __construct( string $db_name, ?PDO $pdo = null ) {
$this->prepare_directory();
}
- $locked = false;
- $status = 0;
- $err_message = '';
- do {
- try {
- $options = array(
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
- PDO::ATTR_STRINGIFY_FETCHES => true,
- PDO::ATTR_TIMEOUT => 5,
- );
-
- $dsn = 'sqlite:' . FQDB;
- $pdo = new PDO( $dsn, null, null, $options );
- } catch ( PDOException $ex ) {
- $status = $ex->getCode();
- if ( self::SQLITE_BUSY === $status || self::SQLITE_LOCKED === $status ) {
- $locked = true;
- } else {
- $err_message = $ex->getMessage();
- }
- }
- } while ( $locked );
+ try {
+ $options = array(
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_STRINGIFY_FETCHES => true,
+ PDO::ATTR_TIMEOUT => self::DEFAULT_TIMEOUT,
+ );
- if ( $status > 0 ) {
- $message = sprintf(
+ $dsn = 'sqlite:' . FQDB;
+ $pdo = new PDO( $dsn, null, null, $options );
+ } catch ( PDOException $ex ) {
+ $this->error_messages[] = sprintf(
'%s
%s
%s
',
'Database initialization error!',
- "Code: $status",
- "Error Message: $err_message"
+ 'Code: ' . $ex->getCode(),
+ 'Error Message: ' . $ex->getMessage()
);
- $this->is_error = true;
- $this->error_messages[] = $message;
return;
}
}
@@ -603,17 +589,7 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
// Perform all the queries in a nested transaction.
$this->begin_transaction();
-
- do {
- $error = null;
- try {
- $this->execute_mysql_query( $ast );
- } catch ( PDOException $error ) {
- if ( $error->getCode() !== self::SQLITE_BUSY ) {
- throw $error;
- }
- }
- } while ( $error );
+ $this->execute_mysql_query( $ast );
if ( function_exists( 'do_action' ) ) {
/**
From d19c9aa4d87ca643b342a55ea87062f68ea8f9ce Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 4 Feb 2025 09:51:20 +0100
Subject: [PATCH 104/124] Remove legacy hooks
---
.../sqlite-ast/class-wp-sqlite-driver.php | 80 -------------------
1 file changed, 80 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index cc63924..757f360 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -504,35 +504,6 @@ public function get_affected_rows() {
*/
public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) {
$this->flush();
- if ( function_exists( 'apply_filters' ) ) {
- /**
- * Filters queries before they are translated and run.
- *
- * Return a non-null value to cause query() to return early with that result.
- * Use this filter to intercept queries that don't work correctly in SQLite.
- *
- * From within the filter you can do
- * function filter_sql ($result, $translator, $statement, $mode, $fetch_mode_args) {
- * if ( intercepting this query ) {
- * return $translator->execute_sqlite_query( $statement );
- * }
- * return $result;
- * }
- *
- * @param null|array $result Default null to continue with the query.
- * @param object $translator The translator object. You can call $translator->execute_sqlite_query().
- * @param string $query The statement passed.
- * @param int $fetch_mode Fetch mode: PDO::FETCH_OBJ, PDO::FETCH_CLASS, etc.
- * @param array $fetch_mode_args Variable arguments passed to query.
- *
- * @returns null|array Null to proceed, or an array containing a resultset.
- * @since 2.1.0
- */
- $pre = apply_filters( 'pre_query_sqlite_db', null, $this, $query, $fetch_mode, $fetch_mode_args );
- if ( null !== $pre ) {
- return $pre;
- }
- }
$this->pdo_fetch_mode = $fetch_mode;
$this->mysql_query = $query;
@@ -590,37 +561,9 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
// Perform all the queries in a nested transaction.
$this->begin_transaction();
$this->execute_mysql_query( $ast );
-
- if ( function_exists( 'do_action' ) ) {
- /**
- * Notifies that a query has been translated and executed.
- *
- * @param string $query The executed SQL query.
- * @param string $query_type The type of the SQL query (e.g. SELECT, INSERT, UPDATE, DELETE).
- * @param string $table_name The name of the table affected by the SQL query.
- * @param array $insert_columns The columns affected by the INSERT query (if applicable).
- * @param int $last_insert_id The ID of the last inserted row (if applicable).
- * @param int $affected_rows The number of affected rows (if applicable).
- *
- * @since 0.1.0
- */
- do_action(
- 'sqlite_translated_query_executed',
- $this->mysql_query,
- $this->query_type,
- $this->table_name,
- $this->insert_columns,
- $this->last_insert_id,
- $this->affected_rows
- );
- }
-
- // Commit the nested transaction.
$this->commit();
-
return $this->return_value;
} catch ( Exception $err ) {
- // Rollback the nested transaction.
$this->rollback();
if ( defined( 'PDO_DEBUG' ) && PDO_DEBUG === true ) {
throw $err;
@@ -758,18 +701,6 @@ public function begin_transaction() {
} finally {
if ( $success ) {
++$this->transaction_level;
- if ( function_exists( 'do_action' ) ) {
- /**
- * Notifies that a transaction-related query has been translated and executed.
- *
- * @param string $command The SQL statement (one of "START TRANSACTION", "COMMIT", "ROLLBACK").
- * @param bool $success Whether the SQL statement was successful or not.
- * @param int $nesting_level The nesting level of the transaction.
- *
- * @since 0.1.0
- */
- do_action( 'sqlite_transaction_query_executed', 'START TRANSACTION', (bool) $this->last_exec_returned, $this->transaction_level - 1 );
- }
}
}
return $success;
@@ -791,10 +722,6 @@ public function commit() {
} else {
$this->execute_sqlite_query( 'RELEASE SAVEPOINT LEVEL' . $this->transaction_level );
}
-
- if ( function_exists( 'do_action' ) ) {
- do_action( 'sqlite_transaction_query_executed', 'COMMIT', (bool) $this->last_exec_returned, $this->transaction_level );
- }
return $this->last_exec_returned;
}
@@ -814,9 +741,6 @@ public function rollback() {
} else {
$this->execute_sqlite_query( 'ROLLBACK TO SAVEPOINT LEVEL' . $this->transaction_level );
}
- if ( function_exists( 'do_action' ) ) {
- do_action( 'sqlite_transaction_query_executed', 'ROLLBACK', (bool) $this->last_exec_returned, $this->transaction_level );
- }
return $this->last_exec_returned;
}
@@ -1016,10 +940,6 @@ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): vo
if ( is_numeric( $this->last_insert_id ) ) {
$this->last_insert_id = (int) $this->last_insert_id;
}
-
- if ( function_exists( 'apply_filters' ) ) {
- $this->last_insert_id = apply_filters( 'sqlite_last_insert_id', $this->last_insert_id, $this->table_name );
- }
}
private function execute_update_statement( WP_Parser_Node $node ): void {
From 67c1fffdfdf8c9acf3fcf3a992e6271741626f2d Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 4 Feb 2025 11:03:39 +0100
Subject: [PATCH 105/124] Closely mirror wpdb::query() and wpdb::_do_query()
---
wp-includes/sqlite/class-wp-sqlite-db.php | 111 +++++++++++++++++-----
1 file changed, 86 insertions(+), 25 deletions(-)
diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php
index 77567eb..e30ab90 100644
--- a/wp-includes/sqlite/class-wp-sqlite-db.php
+++ b/wp-includes/sqlite/class-wp-sqlite-db.php
@@ -263,16 +263,17 @@ public function check_connection( $allow_bail = true ) {
}
/**
- * Method to execute the query.
+ * Performs a database query.
*
- * This overrides wpdb::query(). In fact, this method does all the database
- * access jobs.
+ * This overrides wpdb::query() while closely mirroring its implementation.
*
* @see wpdb::query()
*
* @param string $query Database query.
*
- * @return int|false Number of rows affected/selected or false on error
+ * @param string $query Database query.
+ * @return int|bool Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. Number of rows
+ * affected/selected for all other queries. Boolean false on error.
*/
public function query( $query ) {
if ( ! $this->ready ) {
@@ -281,43 +282,103 @@ public function query( $query ) {
$query = apply_filters( 'query', $query );
- $this->flush();
+ if ( ! $query ) {
+ $this->insert_id = 0;
+ return false;
+ }
- $this->func_call = "\$db->query(\"$query\")";
- $this->last_query = $query;
+ $this->flush();
- if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
- $this->timer_start();
- }
+ // Log how the function was called.
+ $this->func_call = "\$db->query(\"$query\")";
- $this->result = $this->dbh->query( $query );
- ++$this->num_queries;
+ // Keep track of the last query for debug.
+ $this->last_query = $query;
- if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
- $this->queries[] = array( $query, $this->timer_stop(), $this->get_caller() );
- }
+ /*
+ * @TODO: WPDB uses "$this->check_current_query" to check table/column
+ * charset and strip all invalid characters from the query.
+ * This is an involved process that we can bypass for SQLite,
+ * if we simply strip all invalid UTF-8 characters from the query.
+ *
+ * To do so, mb_convert_encoding can be used with an optional
+ * fallback to a htmlspecialchars method. E.g.:
+ * https://github.com/nette/utils/blob/be534713c227aeef57ce1883fc17bc9f9e29eca2/src/Utils/Strings.php#L42
+ */
+ $this->_do_query( $query );
$this->last_error = $this->dbh->get_error_message();
if ( $this->last_error ) {
- $this->print_error( $this->last_error );
+ // Clear insert_id on a subsequent failed insert.
+ if ( $this->insert_id && preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
+ $this->insert_id = 0;
+ }
+
+ $this->print_error();
return false;
}
- if ( preg_match( '/^\\s*(set|create|alter|truncate|drop|optimize)\\s*/i', $query ) ) {
- return $this->dbh->get_return_value();
- }
+ if ( preg_match( '/^\s*(create|alter|truncate|drop)\s/i', $query ) ) {
+ $return_val = $this->result;
+ } elseif ( preg_match( '/^\s*(insert|delete|update|replace)\s/i', $query ) ) {
+ if ( $this->dbh instanceof WP_SQLite_Driver ) {
+ $this->rows_affected = $this->dbh->get_return_value();
+ } else {
+ $this->rows_affected = $this->dbh->get_rows_affected();
+ }
- if ( preg_match( '/^\\s*(insert|delete|update|replace)\s/i', $query ) ) {
- $this->rows_affected = $this->dbh->get_affected_rows();
+ // Take note of the insert_id.
if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
$this->insert_id = $this->dbh->get_insert_id();
}
- return $this->rows_affected;
+
+ // Return number of rows affected.
+ $return_val = $this->rows_affected;
+ } else {
+ $num_rows = 0;
+
+ if ( is_array( $this->result ) ) {
+ $this->last_result = $this->result;
+ $num_rows = count( $this->result );
+ }
+
+ // Log and return the number of rows selected.
+ $this->num_rows = $num_rows;
+ $return_val = $num_rows;
+ }
+
+ return $return_val;
+ }
+
+ /**
+ * Internal function to perform the SQLite query call.
+ *
+ * This closely mirrors wpdb::_do_query().
+ *
+ * @see wpdb::_do_query()
+ *
+ * @param string $query The query to run.
+ */
+ private function _do_query( $query ) {
+ if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
+ $this->timer_start();
+ }
+
+ if ( ! empty( $this->dbh ) ) {
+ $this->result = $this->dbh->query( $query );
}
- $this->last_result = $this->dbh->get_query_results();
- $this->num_rows = $this->dbh->get_num_rows();
- return $this->num_rows;
+ ++$this->num_queries;
+
+ if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
+ $this->log_query(
+ $query,
+ $this->timer_stop(),
+ $this->get_caller(),
+ $this->time_start,
+ array()
+ );
+ }
}
/**
From 0f8dc09d75db2ca100754bae6c8472afeadb7585 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 4 Feb 2025 13:21:32 +0100
Subject: [PATCH 106/124] Simplify driver state
---
tests/WP_SQLite_Driver_Translation_Tests.php | 4 +-
tests/tools/dump-sqlite-query.php | 2 +-
.../sqlite-ast/class-wp-sqlite-driver.php | 219 ++++--------------
3 files changed, 48 insertions(+), 177 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 45ffbfc..0da6b1a 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -1255,7 +1255,7 @@ private function assertQuery( $expected, string $query ): void {
);
}
- $executed_queries = array_column( $this->driver->get_executed_sqlite_queries(), 'sql' );
+ $executed_queries = array_column( $this->driver->get_sqlite_queries(), 'sql' );
// Remove BEGIN and COMMIT/ROLLBACK queries.
if ( count( $executed_queries ) > 2 ) {
@@ -1299,7 +1299,7 @@ function ( $query ) {
private function assertExecutedInformationSchemaQueries( array $expected ): void {
// Collect and normalize "information_schema" queries.
$queries = array();
- foreach ( $this->driver->get_executed_sqlite_queries() as $query ) {
+ foreach ( $this->driver->get_sqlite_queries() as $query ) {
if ( ! str_contains( $query['sql'], '_mysql_information_schema_' ) ) {
continue;
}
diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php
index d222ae8..8f1aaaf 100644
--- a/tests/tools/dump-sqlite-query.php
+++ b/tests/tools/dump-sqlite-query.php
@@ -17,7 +17,7 @@
$driver->query( $query );
-$executed_queries = $driver->get_executed_sqlite_queries();
+$executed_queries = $driver->get_sqlite_queries();
if ( count( $executed_queries ) > 2 ) {
// Remove BEGIN and COMMIT/ROLLBACK queries.
$executed_queries = array_values( array_slice( $executed_queries, 1, -1, true ) );
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 757f360..d2895f6 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -244,28 +244,7 @@ class WP_SQLite_Driver {
*
* @var array
*/
- private $executed_sqlite_queries = array();
-
- /**
- * The affected table name.
- *
- * @var array
- */
- private $table_name = array();
-
- /**
- * The type of the executed query (SELECT, INSERT, etc).
- *
- * @var array
- */
- private $query_type = array();
-
- /**
- * The columns to insert.
- *
- * @var array
- */
- private $insert_columns = array();
+ private $sqlite_queries = array();
/**
* Class variable to store the result of the query.
@@ -276,13 +255,6 @@ class WP_SQLite_Driver {
*/
private $results = null;
- /**
- * Class variable to check if there is an error.
- *
- * @var boolean
- */
- private $is_error = false;
-
/**
* Class variable to store the file name and function to cause error.
*
@@ -301,21 +273,6 @@ class WP_SQLite_Driver {
*/
private $error_messages = array();
- /**
- * Class variable to store the affected row id.
- *
- * @var int integer
- * @access private
- */
- private $last_insert_id;
-
- /**
- * Number of rows found by the last SELECT query.
- *
- * @var int
- */
- private $last_select_found_rows;
-
/**
* Number of rows found by the last SQL_CALC_FOUND_ROW query.
*
@@ -323,20 +280,6 @@ class WP_SQLite_Driver {
*/
private $last_sql_calc_found_rows = null;
- /**
- * Class variable to store the number of rows affected.
- *
- * @var int integer
- */
- private $affected_rows;
-
- /**
- * Variable to emulate MySQL affected row.
- *
- * @var integer
- */
- private $num_rows;
-
/**
* Return value from query().
*
@@ -353,13 +296,6 @@ class WP_SQLite_Driver {
*/
private $transaction_level = 0;
- /**
- * Value returned by the last exec().
- *
- * @var mixed
- */
- private $last_exec_returned;
-
/**
* The PDO fetch mode passed to query().
*
@@ -471,22 +407,19 @@ public function get_mysql_query(): ?string {
*
* @return array
*/
- public function get_executed_sqlite_queries(): array {
- return $this->executed_sqlite_queries;
+ public function get_sqlite_queries(): array {
+ return $this->sqlite_queries;
}
/**
* Method to return inserted row id.
*/
public function get_insert_id() {
- return $this->last_insert_id;
- }
-
- /**
- * Method to return the number of rows affected.
- */
- public function get_affected_rows() {
- return $this->affected_rows;
+ $last_insert_id = $this->pdo->lastInsertId();
+ if ( is_numeric( $last_insert_id ) ) {
+ $last_insert_id = (int) $last_insert_id;
+ }
+ return $last_insert_id;
}
/**
@@ -581,13 +514,6 @@ public function get_query_results() {
return $this->results;
}
- /**
- * Method to return the number of rows from the queried result.
- */
- public function get_num_rows() {
- return $this->num_rows;
- }
-
/**
* Method to return the queried results according to the query types.
*
@@ -611,20 +537,19 @@ public function get_return_value() {
* }
*/
public function execute_sqlite_query( $sql, $params = array() ) {
- $this->executed_sqlite_queries[] = array(
+ $this->sqlite_queries[] = array(
'sql' => $sql,
'params' => $params,
);
$stmt = $this->pdo->prepare( $sql );
if ( false === $stmt || null === $stmt ) {
- $this->last_exec_returned = null;
- $info = $this->pdo->errorInfo();
+ $info = $this->pdo->errorInfo();
throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] );
}
- $this->last_exec_returned = $stmt->execute( $params );
- if ( false === $this->last_exec_returned ) {
+ $is_success = $stmt->execute( $params );
+ if ( false === $is_success ) {
$info = $stmt->errorInfo();
throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] );
}
@@ -641,22 +566,17 @@ public function execute_sqlite_query( $sql, $params = array() ) {
*/
public function get_error_message() {
if ( count( $this->error_messages ) === 0 ) {
- $this->is_error = false;
$this->error_messages = array();
return '';
}
- if ( false === $this->is_error ) {
- return '';
- }
-
$output = '
' . PHP_EOL;
$output .= '' . PHP_EOL;
$output .= '
MySQL query:
' . PHP_EOL;
$output .= '
' . $this->mysql_query . '
' . PHP_EOL;
$output .= '
Queries made or created this session were:
' . PHP_EOL;
$output .= '
' . PHP_EOL;
- foreach ( $this->executed_sqlite_queries as $q ) {
+ foreach ( $this->sqlite_queries as $q ) {
$message = "Executing: {$q['sql']} | " . ( $q['params'] ? 'parameters: ' . implode( ', ', $q['params'] ) : '(no parameters)' );
$output .= '- ' . htmlspecialchars( $message ) . '
' . PHP_EOL;
@@ -686,34 +606,22 @@ public function get_error_message() {
/**
* Begin a new transaction or nested transaction.
- *
- * @return boolean
*/
- public function begin_transaction() {
- $success = false;
- try {
- if ( 0 === $this->transaction_level ) {
- $this->execute_sqlite_query( 'BEGIN' );
- } else {
- $this->execute_sqlite_query( 'SAVEPOINT LEVEL' . $this->transaction_level );
- }
- $success = $this->last_exec_returned;
- } finally {
- if ( $success ) {
- ++$this->transaction_level;
- }
+ public function begin_transaction(): void {
+ if ( 0 === $this->transaction_level ) {
+ $this->execute_sqlite_query( 'BEGIN' );
+ } else {
+ $this->execute_sqlite_query( 'SAVEPOINT LEVEL' . $this->transaction_level );
}
- return $success;
+ ++$this->transaction_level;
}
/**
* Commit the current transaction or nested transaction.
- *
- * @return boolean True on success, false on failure.
*/
- public function commit() {
+ public function commit(): void {
if ( 0 === $this->transaction_level ) {
- return false;
+ return;
}
--$this->transaction_level;
@@ -722,17 +630,14 @@ public function commit() {
} else {
$this->execute_sqlite_query( 'RELEASE SAVEPOINT LEVEL' . $this->transaction_level );
}
- return $this->last_exec_returned;
}
/**
* Rollback the current transaction or nested transaction.
- *
- * @return boolean True on success, false on failure.
*/
- public function rollback() {
+ public function rollback(): void {
if ( 0 === $this->transaction_level ) {
- return false;
+ return;
}
--$this->transaction_level;
@@ -741,7 +646,6 @@ public function rollback() {
} else {
$this->execute_sqlite_query( 'ROLLBACK TO SAVEPOINT LEVEL' . $this->transaction_level );
}
- return $this->last_exec_returned;
}
/**
@@ -764,28 +668,20 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$ast = $children[0]->get_child_node();
switch ( $ast->rule_name ) {
case 'selectStatement':
- $this->query_type = 'SELECT';
$this->execute_select_statement( $ast );
break;
case 'insertStatement':
- $this->query_type = 'INSERT';
+ case 'replaceStatement':
$this->execute_insert_or_replace_statement( $ast );
break;
case 'updateStatement':
- $this->query_type = 'UPDATE';
$this->execute_update_statement( $ast );
break;
- case 'replaceStatement':
- $this->query_type = 'REPLACE';
- $this->execute_insert_or_replace_statement( $ast );
- break;
case 'deleteStatement':
- $this->query_type = 'DELETE';
$this->execute_delete_statement( $ast );
break;
case 'createStatement':
- $this->query_type = 'CREATE';
- $subtree = $ast->get_child_node();
+ $subtree = $ast->get_child_node();
switch ( $subtree->rule_name ) {
case 'createTable':
$this->execute_create_table_statement( $ast );
@@ -801,8 +697,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
}
break;
case 'alterStatement':
- $this->query_type = 'ALTER';
- $subtree = $ast->get_child_node();
+ $subtree = $ast->get_child_node();
switch ( $subtree->rule_name ) {
case 'alterTable':
$this->execute_alter_table_statement( $ast );
@@ -818,8 +713,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
}
break;
case 'dropStatement':
- $this->query_type = 'DROP';
- $subtree = $ast->get_child_node();
+ $subtree = $ast->get_child_node();
switch ( $subtree->rule_name ) {
case 'dropTable':
$this->execute_drop_table_statement( $ast );
@@ -838,12 +732,10 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$this->results = 0;
break;
case 'showStatement':
- $this->query_type = 'SHOW';
$this->execute_show_statement( $ast );
break;
case 'utilityStatement':
- $this->query_type = 'DESCRIBE';
- $subtree = $ast->get_child_node();
+ $subtree = $ast->get_child_node();
switch ( $subtree->rule_name ) {
case 'describeStatement':
$this->execute_describe_statement( $subtree );
@@ -935,11 +827,6 @@ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): vo
$query = implode( ' ', $parts );
$this->execute_sqlite_query( $query );
$this->set_result_from_affected_rows();
-
- $this->last_insert_id = $this->pdo->lastInsertId();
- if ( is_numeric( $this->last_insert_id ) ) {
- $this->last_insert_id = (int) $this->last_insert_id;
- }
}
private function execute_update_statement( WP_Parser_Node $node ): void {
@@ -1069,7 +956,7 @@ private function execute_delete_statement( WP_Parser_Node $node ): void {
)
);
$this->set_result_from_affected_rows();
- $rows += $this->affected_rows;
+ $rows += $this->results;
}
}
@@ -1120,12 +1007,11 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void {
$constraint_queries = array_slice( $queries, 1 );
$this->execute_sqlite_query( $create_table_query );
- $this->results = $this->last_exec_returned;
- $this->return_value = $this->results;
foreach ( $constraint_queries as $query ) {
$this->execute_sqlite_query( $query );
}
+ $this->return_value = '1';
}
private function execute_alter_table_statement( WP_Parser_Node $node ): void {
@@ -1289,7 +1175,6 @@ private function execute_drop_table_statement( WP_Parser_Node $node ): void {
foreach ( $queries as $query ) {
$this->execute_sqlite_query( $query );
}
- $this->results = $this->last_exec_returned;
$this->information_schema_builder->record_drop_table( $node );
}
@@ -1954,10 +1839,11 @@ private function translate_function_call( WP_Parser_Node $node ): string {
// that the function is used in the SELECT field list.
// For compatibility with more complex use cases, it may
// be better to register it as a custom SQLite function.
- return sprintf(
- "(SELECT %d) AS 'FOUND_ROWS()'",
- $this->last_sql_calc_found_rows ?? $this->last_select_found_rows
- );
+ $found_rows = $this->last_sql_calc_found_rows;
+ if ( null === $found_rows && is_array( $this->results ) ) {
+ $found_rows = count( $this->results );
+ }
+ return sprintf( "(SELECT %d) AS 'FOUND_ROWS()'", $found_rows );
default:
return $this->translate_sequence( $node->get_children() );
}
@@ -2420,18 +2306,11 @@ private function prepare_directory() {
* Method to clear previous data.
*/
private function flush() {
- $this->mysql_query = '';
- $this->results = null;
- $this->last_exec_returned = null;
- $this->table_name = null;
- $this->last_insert_id = null;
- $this->affected_rows = null;
- $this->insert_columns = array();
- $this->num_rows = null;
- $this->return_value = null;
- $this->error_messages = array();
- $this->is_error = false;
- $this->executed_sqlite_queries = array();
+ $this->mysql_query = '';
+ $this->sqlite_queries = array();
+ $this->results = null;
+ $this->return_value = null;
+ $this->error_messages = array();
}
/**
@@ -2440,13 +2319,7 @@ private function flush() {
* @param array $data The data to set.
*/
private function set_results_from_fetched_data( $data ) {
- if ( null === $this->results ) {
- $this->results = $data;
- }
- if ( is_array( $this->results ) ) {
- $this->num_rows = count( $this->results );
- $this->last_select_found_rows = count( $this->results );
- }
+ $this->results = $data;
$this->return_value = $this->results;
}
@@ -2463,13 +2336,12 @@ private function set_result_from_affected_rows( $override = null ) {
* Source: https://www.php.net/manual/en/pdostatement.rowcount.php
*/
if ( null === $override ) {
- $this->affected_rows = (int) $this->execute_sqlite_query( 'select changes()' )->fetch()[0];
+ $affected_rows = (int) $this->execute_sqlite_query( 'select changes()' )->fetch()[0];
} else {
- $this->affected_rows = $override;
+ $affected_rows = $override;
}
- $this->return_value = $this->affected_rows;
- $this->num_rows = $this->affected_rows;
- $this->results = $this->affected_rows;
+ $this->results = $affected_rows;
+ $this->return_value = $affected_rows;
}
/**
@@ -2504,7 +2376,6 @@ private function set_error( $line, $function_name, $message ) {
'function' => $function_name,
);
$this->error_messages[] = $message;
- $this->is_error = true;
}
private function invalid_input_exception() {
From 59c4451dc218eb4c909bcbb0589d76dfd7691d0e Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 4 Feb 2025 15:04:16 +0100
Subject: [PATCH 107/124] Remove uselsess return values
---
tests/WP_SQLite_Driver_Tests.php | 51 +++++++++----------
.../sqlite-ast/class-wp-sqlite-driver.php | 4 --
wp-includes/sqlite/class-wp-sqlite-db.php | 2 +-
3 files changed, 26 insertions(+), 31 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 38df86d..a14bed0 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -849,7 +849,7 @@ public function testCreateTable() {
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci"
);
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
$this->assertQuery( 'DESCRIBE wptests_users;' );
$results = $this->engine->get_query_results();
@@ -948,7 +948,7 @@ public function testCreateTableWithTrailingComma() {
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci'
);
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
}
public function testCreateTableSpatialIndex() {
@@ -959,7 +959,7 @@ public function testCreateTableSpatialIndex() {
)'
);
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
}
public function testCreateTableWithMultiValueColumnTypeModifiers() {
@@ -973,7 +973,7 @@ enum_column ENUM('a', 'b', 'c') NOT NULL DEFAULT 'a',
)"
);
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
$this->assertQuery( 'DESCRIBE wptests_users;' );
$results = $this->engine->get_query_results();
@@ -1022,10 +1022,11 @@ public function testAlterTableAddAndDropColumn() {
name varchar(20) NOT NULL default ''
);"
);
+ $this->assertNull( $result );
$result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD COLUMN `column` int;' );
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
$this->assertQuery( 'DESCRIBE _tmp_table;' );
$results = $this->engine->get_query_results();
@@ -1053,7 +1054,7 @@ public function testAlterTableAddAndDropColumn() {
$result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD `column2` int;' );
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
$this->assertQuery( 'DESCRIBE _tmp_table;' );
$results = $this->engine->get_query_results();
@@ -1089,7 +1090,7 @@ public function testAlterTableAddAndDropColumn() {
$result = $this->assertQuery( 'ALTER TABLE _tmp_table DROP COLUMN `column`;' );
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
$this->assertQuery( 'DESCRIBE _tmp_table;' );
$results = $this->engine->get_query_results();
@@ -1117,7 +1118,7 @@ public function testAlterTableAddAndDropColumn() {
$result = $this->assertQuery( 'ALTER TABLE _tmp_table DROP `column2`;' );
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
$this->assertQuery( 'DESCRIBE _tmp_table;' );
$results = $this->engine->get_query_results();
@@ -1145,7 +1146,7 @@ public function testAlterTableAddNotNullVarcharColumn() {
$result = $this->assertQuery( "ALTER TABLE _tmp_table ADD COLUMN `column` VARCHAR(20) NOT NULL DEFAULT 'foo';" );
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
$this->assertQuery( 'DESCRIBE _tmp_table;' );
$results = $this->engine->get_query_results();
@@ -1726,7 +1727,7 @@ public function testAlterTableAddIndex() {
$result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD INDEX name (name);' );
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
$this->assertQuery( 'SHOW INDEX FROM _tmp_table;' );
$results = $this->engine->get_query_results();
@@ -1763,7 +1764,7 @@ public function testAlterTableAddUniqueIndex() {
$result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD UNIQUE INDEX name (name(20));' );
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
$this->assertQuery( 'SHOW INDEX FROM _tmp_table;' );
$results = $this->engine->get_query_results();
@@ -1800,7 +1801,7 @@ public function testAlterTableAddFulltextIndex() {
$result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD FULLTEXT INDEX name (name);' );
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
$this->assertQuery( 'SHOW INDEX FROM _tmp_table;' );
$results = $this->engine->get_query_results();
@@ -1853,7 +1854,7 @@ public function testAlterTableModifyColumn() {
// Rename the "name" field to "firstname":
$result = $this->engine->query( "ALTER TABLE _tmp_table CHANGE column name firstname varchar(50) NOT NULL default 'mark';" );
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
// Confirm the original data is still there:
$result = $this->engine->query( 'SELECT * FROM _tmp_table;' );
@@ -1904,7 +1905,7 @@ public function testAlterTableModifyColumnWithSkippedColumnKeyword() {
// Rename the "name" field to "firstname":
$result = $this->engine->query( "ALTER TABLE _tmp_table CHANGE name firstname varchar(50) NOT NULL default 'mark';" );
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
// Confirm the original data is still there:
$result = $this->engine->query( 'SELECT * FROM _tmp_table;' );
@@ -1936,13 +1937,13 @@ public function testAlterTableModifyColumnWithHyphens() {
)'
);
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
$result = $this->assertQuery(
'ALTER TABLE wptests_dbdelta_test2 CHANGE COLUMN `foo-bar` `foo-bar` text DEFAULT NULL'
);
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
$result = $this->assertQuery( 'DESCRIBE wptests_dbdelta_test2;' );
$this->assertEquals( '', $this->engine->get_error_message() );
@@ -1973,21 +1974,21 @@ public function testAlterTableModifyColumnComplexChange() {
);"
);
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
// Add a unique index
$result = $this->assertQuery(
'ALTER TABLE _tmp_table ADD UNIQUE INDEX "test_unique_composite" (name, lastname);'
);
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
// Add a regular index
$result = $this->assertQuery(
'ALTER TABLE _tmp_table ADD INDEX "test_regular" (lastname);'
);
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
// Confirm the table is well-behaved so far:
@@ -2019,11 +2020,11 @@ public function testAlterTableModifyColumnComplexChange() {
// Now – let's change a few columns:
$result = $this->engine->query( 'ALTER TABLE _tmp_table CHANGE COLUMN name firstname varchar(20)' );
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
$result = $this->engine->query( 'ALTER TABLE _tmp_table CHANGE COLUMN date_as_string datetime datetime NOT NULL' );
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
// Finally, let's confirm our data is intact and the table is still well-behaved:
$result = $this->engine->query( 'SELECT * FROM _tmp_table ORDER BY ID;' );
@@ -2058,7 +2059,7 @@ public function testCaseInsensitiveUniqueIndex() {
UNIQUE KEY last (lastname)
);"
);
- $this->assertEquals( 1, $result );
+ $this->assertNull( $result );
$result1 = $this->engine->query( "INSERT INTO _tmp_table (name, lastname) VALUES ('first', 'last');" );
$this->assertEquals( 1, $result1 );
@@ -3089,10 +3090,8 @@ public function testTranslatesDoubleAlterTable() {
'
);
$this->assertEquals( '', $this->engine->get_error_message() );
- $this->assertEquals(
- 1,
- $result
- );
+ $this->assertNull( $result );
+
$result = $this->assertQuery(
'SHOW INDEX FROM _options'
);
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index d2895f6..f858bab 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1011,7 +1011,6 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void {
foreach ( $constraint_queries as $query ) {
$this->execute_sqlite_query( $query );
}
- $this->return_value = '1';
}
private function execute_alter_table_statement( WP_Parser_Node $node ): void {
@@ -1131,9 +1130,6 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void {
// @TODO: Triggers and views.
- $this->results = 1;
- $this->return_value = $this->results;
-
// @TODO: Consider using a "fast path" for ALTER TABLE statements that
// consist only of operations that SQLite's ALTER TABLE supports.
}
diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php
index e30ab90..cdb214d 100644
--- a/wp-includes/sqlite/class-wp-sqlite-db.php
+++ b/wp-includes/sqlite/class-wp-sqlite-db.php
@@ -319,7 +319,7 @@ public function query( $query ) {
}
if ( preg_match( '/^\s*(create|alter|truncate|drop)\s/i', $query ) ) {
- $return_val = $this->result;
+ $return_val = true;
} elseif ( preg_match( '/^\s*(insert|delete|update|replace)\s/i', $query ) ) {
if ( $this->dbh instanceof WP_SQLite_Driver ) {
$this->rows_affected = $this->dbh->get_return_value();
From a83a5f8485a0e3b1019707ff20cc15c27bcdbe06 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Tue, 4 Feb 2025 15:10:13 +0100
Subject: [PATCH 108/124] Change FIX comments to CHANGED
---
grammar-tools/MySQLParser.g4 | 132 +++++++++++++++++------------------
1 file changed, 66 insertions(+), 66 deletions(-)
diff --git a/grammar-tools/MySQLParser.g4 b/grammar-tools/MySQLParser.g4
index efdc45b..9279fc5 100644
--- a/grammar-tools/MySQLParser.g4
+++ b/grammar-tools/MySQLParser.g4
@@ -153,12 +153,12 @@ alterStatement:
| alterLogfileGroup
| alterServer
// ALTER USER is part of the user management rule.
- | alterInstance /* @FIX: Add support for "ALTER INSTANCE ..." statement. */
+ | alterInstance /* @CHANGED: Add support for "ALTER INSTANCE ..." statement. */
)
;
/*
- * @FIX:
+ * @CHANGED:
* Add support for "ALTER INSTANCE ..." statement.
*/
alterInstance:
@@ -172,7 +172,7 @@ alterInstance:
;
alterDatabase:
- /* @FIX: Make "schemaRef" optional. */
+ /* @CHANGED: Make "schemaRef" optional. */
DATABASE_SYMBOL schemaRef? (
createDatabaseOption+
| {serverVersion < 80000}? UPGRADE_SYMBOL DATA_SYMBOL DIRECTORY_SYMBOL NAME_SYMBOL
@@ -218,7 +218,7 @@ alterTable:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix "alterTableActions" to solve conflicts between "alterCommandsModifierList" and "alterCommandList".
*/
alterTableActions:
@@ -234,7 +234,7 @@ alterTableActions:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix "alterCommandList" to solve conflicts between "alterCommandsModifierList" prefixes.
*/
alterCommandList:
@@ -320,7 +320,7 @@ alterListItem:
| signedLiteral
)
| DROP_SYMBOL DEFAULT_SYMBOL
- | {serverVersion >= 80023}? SET_SYMBOL visibility /* @FIX: Add missing SET VISIBLE/INVISIBLE clause. */
+ | {serverVersion >= 80023}? SET_SYMBOL visibility /* @CHANGED: Add missing SET VISIBLE/INVISIBLE clause. */
)
| {serverVersion >= 80000}? ALTER_SYMBOL INDEX_SYMBOL indexRef visibility
| {serverVersion >= 80017}? ALTER_SYMBOL CHECK_SYMBOL identifier constraintEnforcement
@@ -352,7 +352,7 @@ restrict:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix ALTER TABLE with ORDER to use 'qualifiedIdentifier' instead of just 'identifier'.
* This is necessary to support "t.id" in a query like "ALTER TABLE t ORDER BY t.id".
*/
@@ -425,7 +425,7 @@ alterTablespaceOption:
| tsOptionAutoextendSize
| tsOptionMaxSize
| tsOptionEngine
- | {serverVersion >= 80021}? tsOptionEngineAttribute /* @FIX: Add missing "ENGINE_ATTRIBUTE" option. */
+ | {serverVersion >= 80021}? tsOptionEngineAttribute /* @CHANGED: Add missing "ENGINE_ATTRIBUTE" option. */
| tsOptionWait
| tsOptionEncryption
;
@@ -495,7 +495,7 @@ createDatabaseOption:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix "createTable" to solve support "LIKE tableRef" and "LIKE (tableRef)".
* They need to come before "tableElementList" to avoid misinterpreting "LIKE".
*/
@@ -529,7 +529,7 @@ createRoutine: // Rule for external use only.
;
/*
- * @FIX:
+ * @CHANGED:
* Add missing "ifNotExists?".
*/
createProcedure:
@@ -539,7 +539,7 @@ createProcedure:
;
/*
- * @FIX:
+ * @CHANGED:
* Add missing "ifNotExists?".
*/
createFunction:
@@ -613,7 +613,7 @@ createIndex:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix "indexNameAndType" to solve conflicts between "indexName USING_SYMBOL"
* and "indexName TYPE_SYMBOL" prefix by moving them to a single branch.
*/
@@ -694,7 +694,7 @@ tablespaceOption:
| tsOptionExtentSize
| tsOptionNodegroup
| tsOptionEngine
- | {serverVersion >= 80021}? tsOptionEngineAttribute /* @FIX: Add missing "ENGINE_ATTRIBUTE" option. */
+ | {serverVersion >= 80021}? tsOptionEngineAttribute /* @CHANGED: Add missing "ENGINE_ATTRIBUTE" option. */
| tsOptionWait
| tsOptionComment
| {serverVersion >= 50707}? tsOptionFileblockSize
@@ -730,7 +730,7 @@ tsOptionEngine:
;
/*
- * @FIX:
+ * @CHANGED:
* Add missing "ENGINE_ATTRIBUTE" option.
*/
tsOptionEngineAttribute:
@@ -774,7 +774,7 @@ viewSuid:
;
/*
- * @FIX:
+ * @CHANGED:
* Add missing "ifNotExists?".
*/
createTrigger:
@@ -961,7 +961,7 @@ deleteStatementOption: // opt_delete_option in sql_yacc.yy, but the name collide
;*/
/*
- * @FIX:
+ * @CHANGED:
* Reorder "selectItemList" and "exprList" to match "selectItemList", as we don't handle versions yet.
*/
doStatement:
@@ -1099,7 +1099,7 @@ replaceStatement:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix "selectStatement" to solve conflicts between "queryExpressionParens" and "selectStatementWithInto".
* Since "queryExpression" already contains "queryExpressionParens" as a subrule, we can remove it here.
*/
@@ -1145,7 +1145,7 @@ selectStatement:
selectStatementWithInto:
OPEN_PAR_SYMBOL selectStatementWithInto CLOSE_PAR_SYMBOL
| queryExpression intoClause lockingClauseList?
- | queryExpression lockingClauseList intoClause /* @FIX: Add missing "queryExpression" prefix. */
+ | queryExpression lockingClauseList intoClause /* @CHANGED: Add missing "queryExpression" prefix. */
;
queryExpression:
@@ -1166,7 +1166,7 @@ queryExpression:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Implement missing "EXCEPT" and "INTERSECT" operators in the grammar.
* Note that "INTERSECT" must have a higher precedence than "UNION" and "EXCEPT",
* and is therefore evaluated first via "queryTerm" as per:
@@ -1188,7 +1188,7 @@ queryTerm:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Rewrite "queryExpressionParens" to keep only "queryExpression" within.
* This avoids conflict between "queryExpressionParens" and "queryExpression"
* (which already contains "queryExpressionParens" as a subrule).
@@ -1278,7 +1278,7 @@ windowSpec:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Rewrite "windowSpecDetails" so to keep variants with "windowName?" last.
* We first need to try to match the symbols as keywords, only then as identifiers.
* Identifiers can never take precedence over keywords in the grammar.
@@ -1544,7 +1544,7 @@ jtOnResponse:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Renamed "unionOption" to "setOperationOption" as this is now used also for EXCEPT and INTERSECT.
*/
setOperationOption:
@@ -1561,7 +1561,7 @@ tableAlias:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix "indexHintList" to use only whitespace as a separator (not commas).
*/
indexHintList:
@@ -1618,7 +1618,7 @@ transactionOrLockingStatement:
;
transactionStatement:
- /* @FIX: Use "transactionCharacteristicList" instead of "transactionCharacteristic". */
+ /* @CHANGED: Use "transactionCharacteristicList" instead of "transactionCharacteristic". */
START_SYMBOL TRANSACTION_SYMBOL transactionCharacteristicList?
| COMMIT_SYMBOL WORK_SYMBOL? (AND_SYMBOL NO_SYMBOL? CHAIN_SYMBOL)? (
NO_SYMBOL? RELEASE_SYMBOL
@@ -1632,7 +1632,7 @@ beginWork:
;
/*
- * @FIX:
+ * @CHANGED:
* Add "transactionCharacteristicList" to fix support for transaction with multiple characteristics.
*/
transactionCharacteristicList:
@@ -1709,7 +1709,7 @@ xid:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix "replicationStatement" to correctly support the "RESET PERSIST" statement.
* The "ifExists" clause wasn't optional, and "identifier" was used instead of "qualifiedIdentifier".
*/
@@ -1814,7 +1814,7 @@ serverIdList:
;
changeReplication:
- /* @FIX: Add support for "CHANGE REPLICATION SOURCE ..." statement. */
+ /* @CHANGED: Add support for "CHANGE REPLICATION SOURCE ..." statement. */
CHANGE_SYMBOL REPLICATION_SYMBOL SOURCE_SYMBOL TO_SYMBOL changeReplicationSourceOptions channel?
| CHANGE_SYMBOL REPLICATION_SYMBOL FILTER_SYMBOL filterDefinition (
COMMA_SYMBOL filterDefinition
@@ -1822,7 +1822,7 @@ changeReplication:
;
/*
- * @FIX:
+ * @CHANGED:
* Add support for "CHANGE REPLICATION SOURCE ..." statement.
*/
changeReplicationSourceOptions:
@@ -1830,7 +1830,7 @@ changeReplicationSourceOptions:
;
/*
- * @FIX:
+ * @CHANGED:
* Add support for "CHANGE REPLICATION SOURCE ..." statement.
*/
replicationSourceOption:
@@ -2013,7 +2013,7 @@ alterUser:
;*/
/*
- * @FIX:
+ * @CHANGED:
* 1. Support also "USER()" function call.
* 2. Fix matching "DEFAULT ROLE" by reordering rules.
* 3. Reorder "alterUserList" and "createUserList" to match "alterUserList", as we don't handle versions yet.
@@ -2040,7 +2040,7 @@ createUser:
//;
/*
- * @FIX:
+ * @CHANGED:
* Add support COMMENT and ATTRIBUTE. The "(COMMENT_SYMBOL | ATTRIBUTE_SYMBOL) textString)?" was missing.
*/
createUserTail:
@@ -2089,7 +2089,7 @@ connectOptions:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Add missing "PASSWORD_LOCK_TIME_SYMBOL" and "FAILED_LOGIN_ATTEMPTS_SYMBOL".
*/
accountLockPasswordExpireOptions:
@@ -2179,7 +2179,7 @@ renameUser:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix "revoke" to support "IF EXISTS" and "REVOKE ALL ON ... FROM ...".
* 1. The "IF EXISTS" clause was missing in the original rule.
* 2. The "(onTypeTo? FROM_SYMBOL userList)?" part was missing in the original rule.
@@ -2214,12 +2214,12 @@ roleOrPrivilegesList:
roleOrPrivilege:
{serverVersion > 80000}? (
- /* @FIX: Reorder branches to solve conflict between them. */
+ /* @CHANGED: Reorder branches to solve conflict between them. */
roleIdentifierOrText (AT_TEXT_SUFFIX | AT_SIGN_SYMBOL textOrIdentifier)
| roleIdentifierOrText columnInternalRefList?
)
| (SELECT_SYMBOL | INSERT_SYMBOL | UPDATE_SYMBOL | REFERENCES_SYMBOL) columnInternalRefList?
- /* @FIX: Moved "CREATE/DROP ROLE" before "CREATE ..." and "DROP ..." to solve conflict. */
+ /* @CHANGED: Moved "CREATE/DROP ROLE" before "CREATE ..." and "DROP ..." to solve conflict. */
| {serverVersion > 80000}? (CREATE_SYMBOL | DROP_SYMBOL) ROLE_SYMBOL
| (
DELETE_SYMBOL
@@ -2257,7 +2257,7 @@ roleOrPrivilege:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Rewrite "grantIdentifier" to solve conflicts between "schemaRef DOT_SYMBOL tableRef"
* and "schemaRef DOT_SYMBOL MULT_OPERATOR". Move them to a single branch, and order
* "schemaRef" and "tableRef" after to preserve precedence of keywords over identifiers.
@@ -2318,7 +2318,7 @@ role:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix administration statements to support both "TABLE" and "TABLES" keywords.
* The original rule only supported "TABLE".
*/
@@ -2343,7 +2343,7 @@ tableAdministrationStatement:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Add missing optional "USING DATA 'json_data'" to UPDATE HISTOGRAM clause.
*/
histogram:
@@ -2369,7 +2369,7 @@ repairType:
installUninstallStatment:
// COMPONENT_SYMBOL is conditionally set in the lexer.
action = INSTALL_SYMBOL type = PLUGIN_SYMBOL identifier SONAME_SYMBOL textStringLiteral
- /* @FIX: Add missing "INSTALL COMPONENT" statement "SET ..." suffix. */
+ /* @CHANGED: Add missing "INSTALL COMPONENT" statement "SET ..." suffix. */
| action = INSTALL_SYMBOL type = COMPONENT_SYMBOL textStringLiteralList (SET_SYMBOL installSetValueList)?
| action = UNINSTALL_SYMBOL type = PLUGIN_SYMBOL pluginRef
| action = UNINSTALL_SYMBOL type = COMPONENT_SYMBOL componentRef (
@@ -2378,7 +2378,7 @@ installUninstallStatment:
;
/*
- * @FIX:
+ * @CHANGED:
* Add missing "INSTALL COMPONENT" statement "SET ..." suffix.
*/
installOptionType: GLOBAL_SYMBOL | PERSIST_SYMBOL;
@@ -2520,7 +2520,7 @@ showStatement:
| value = COLLATION_SYMBOL likeOrWhere?
| {serverVersion < 50700}? value = CONTRIBUTORS_SYMBOL
| value = PRIVILEGES_SYMBOL
- /* @FIX: Moved "GRANTS_SYMBOL ... USING_SYMBOL" before "GRANTS_SYMBOL ..." to solve conflict. */
+ /* @CHANGED: Moved "GRANTS_SYMBOL ... USING_SYMBOL" before "GRANTS_SYMBOL ..." to solve conflict. */
| value = GRANTS_SYMBOL FOR_SYMBOL user USING_SYMBOL userList
| value = GRANTS_SYMBOL (FOR_SYMBOL user)?
/*| value = GRANTS_SYMBOL FOR_SYMBOL user USING_SYMBOL userList */
@@ -2645,7 +2645,7 @@ logType:
;
/*
- * @FIX:
+ * @CHANGED:
* Replace "identifierList" with "tableRefList" to correctly support qualified identifiers.
*/
flushTables:
@@ -2738,7 +2738,7 @@ dropResourceGroup:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Reorder "explainStatement" and "describeStatement".
* EXPLAIN can be followed by an identifier (matching a "describeStatement"),
* but identifiers can never take precedence over keywords in the grammar.
@@ -2774,7 +2774,7 @@ describeStatement:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix "explainStatement" to solve conflict between "ANALYZE ..." and "ANALYZE FORMAT=...".
* The "ANALYZE FORMAT=..." must be attempted to be matched before "ANALYZE ...".
*/
@@ -2823,7 +2823,7 @@ restartServer:
;
/*
- * @FIX:
+ * @CHANGED:
* Factor left recursion.
*/
expr: %expr_simple %expr_rr*;
@@ -2847,7 +2847,7 @@ expr: %expr_simple %expr_rr*;
;*/
/*
- * @FIX:
+ * @CHANGED:
* 1. Factor left recursion.
* 2. Move "compOp (ALL_SYMBOL | ANY_SYMBOL)" before "compOp predicate" to avoid conflicts.
*/
@@ -2904,7 +2904,7 @@ predicateOperations:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Factor left recursion.
*/
bitExpr: simpleExpr %bitExpr_rr*;
@@ -2954,7 +2954,7 @@ bitExpr: simpleExpr %bitExpr_rr*;
;*/
/*
- * @FIX:
+ * @CHANGED:
* Factor left recursion.
*/
simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*;
@@ -2978,7 +2978,7 @@ simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*;
| OPEN_CURLY_SYMBOL identifier expr CLOSE_CURLY_SYMBOL # simpleExprOdbc
| MATCH_SYMBOL identListArg AGAINST_SYMBOL OPEN_PAR_SYMBOL bitExpr fulltextOptions? CLOSE_PAR_SYMBOL # simpleExprMatch
| BINARY_SYMBOL simpleExpr # simpleExprBinary
- /* @FIX: Add support for CAST(... AT TIME ZONE ... AS DATETIME ...). */
+ /* @CHANGED: Add support for CAST(... AT TIME ZONE ... AS DATETIME ...). */
| ({serverVersion >= 80022}?
CAST_SYMBOL OPEN_PAR_SYMBOL expr
AT_SYMBOL TIME_SYMBOL ZONE_SYMBOL INTERVAL_SYMBOL? textStringLiteral
@@ -2991,7 +2991,7 @@ simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*;
| DEFAULT_SYMBOL OPEN_PAR_SYMBOL simpleIdentifier CLOSE_PAR_SYMBOL # simpleExprDefault
| VALUES_SYMBOL OPEN_PAR_SYMBOL simpleIdentifier CLOSE_PAR_SYMBOL # simpleExprValues
| INTERVAL_SYMBOL expr interval PLUS_OPERATOR expr # simpleExprInterval
- /* @FIX: Move function calls and ref to the end to avoid conflicts with the above expressions. */
+ /* @CHANGED: Move function calls and ref to the end to avoid conflicts with the above expressions. */
| functionCall # simpleExprFunction
| runtimeFunctionCall # simpleExprRuntimeFunction
| columnRef jsonOperator? # simpleExprColumnRef
@@ -3077,7 +3077,7 @@ windowingClause:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix "leadLagInfo" to support "identifier" and "userVariable" as well.
*/
leadLagInfo:
@@ -3123,7 +3123,7 @@ runtimeFunctionCall:
| name = HOUR_SYMBOL exprWithParentheses
| name = INSERT_SYMBOL OPEN_PAR_SYMBOL expr COMMA_SYMBOL expr COMMA_SYMBOL expr COMMA_SYMBOL expr CLOSE_PAR_SYMBOL
| name = INTERVAL_SYMBOL OPEN_PAR_SYMBOL expr (COMMA_SYMBOL expr)+ CLOSE_PAR_SYMBOL
- /* @FIX: Add support for "JSON_VALUE(..., '...' RETURNING ). */
+ /* @CHANGED: Add support for "JSON_VALUE(..., '...' RETURNING ). */
| {serverVersion >= 80021}?
name = JSON_VALUE_SYMBOL OPEN_PAR_SYMBOL simpleExpr COMMA_SYMBOL textLiteral (RETURNING_SYMBOL castType)? onEmptyOrError? CLOSE_PAR_SYMBOL
| name = LEFT_SYMBOL OPEN_PAR_SYMBOL expr COMMA_SYMBOL expr CLOSE_PAR_SYMBOL
@@ -3179,7 +3179,7 @@ runtimeFunctionCall:
| name = WEEK_SYMBOL OPEN_PAR_SYMBOL expr (COMMA_SYMBOL expr)? CLOSE_PAR_SYMBOL
| name = WEIGHT_STRING_SYMBOL OPEN_PAR_SYMBOL expr AS_SYMBOL CHAR_SYMBOL CLOSE_PAR_SYMBOL
| name = WEIGHT_STRING_SYMBOL OPEN_PAR_SYMBOL expr (
- /* @FIX: Move "AS BINARY(...)" before "AS CHAR(...)" to solve conflict. */
+ /* @CHANGED: Move "AS BINARY(...)" before "AS CHAR(...)" to solve conflict. */
AS_SYMBOL BINARY_SYMBOL wsNumCodepoints
| (AS_SYMBOL CHAR_SYMBOL wsNumCodepoints)? (
{serverVersion < 80000}? weightStringLevels
@@ -3307,7 +3307,7 @@ elseExpression:
/*
- * @FIX:
+ * @CHANGED:
* Fix CAST(2024 AS YEAR).
* The original grammar was missing the YEAR_SYMBOL in the "castType" rule.
*/
@@ -3672,7 +3672,7 @@ constraintName:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix "fieldDefinition" to solve conflict between "columnAttribute" and GENERATED/AS.
*/
fieldDefinition:
@@ -3681,7 +3681,7 @@ fieldDefinition:
VIRTUAL_SYMBOL
| STORED_SYMBOL
)? (
- /* @FIX: Reorder "columnAttribute*" and "gcolAttribute*" to match "columnAttribute*", as we don't handle versions yet. */
+ /* @CHANGED: Reorder "columnAttribute*" and "gcolAttribute*" to match "columnAttribute*", as we don't handle versions yet. */
{serverVersion >= 80000}? columnAttribute* // Beginning with 8.0 the full attribute set is supported.
| {serverVersion < 80000}? gcolAttribute*
)
@@ -3697,7 +3697,7 @@ columnAttribute:
| NOW_SYMBOL timeFunctionParameters?
| {serverVersion >= 80013}? exprWithParentheses
)
- | {serverVersion >= 80023}? visibility /* @FIX: Add missing VISIBLE/INVISIBLE attribute. */
+ | {serverVersion >= 80023}? visibility /* @CHANGED: Add missing VISIBLE/INVISIBLE attribute. */
| value = ON_SYMBOL UPDATE_SYMBOL NOW_SYMBOL timeFunctionParameters?
| value = AUTO_INCREMENT_SYMBOL
| value = SERIAL_SYMBOL DEFAULT_SYMBOL VALUE_SYMBOL
@@ -3748,7 +3748,7 @@ deleteOption:
(RESTRICT_SYMBOL | CASCADE_SYMBOL)
| SET_SYMBOL nullLiteral
| NO_SYMBOL ACTION_SYMBOL
- | SET_SYMBOL DEFAULT_SYMBOL /* @FIX: Add missing "SET DEFAULT" option. */
+ | SET_SYMBOL DEFAULT_SYMBOL /* @CHANGED: Add missing "SET DEFAULT" option. */
;
keyList:
@@ -3823,7 +3823,7 @@ dataType: // type in sql_yacc.yy
| type = (FLOAT_SYMBOL | DECIMAL_SYMBOL | NUMERIC_SYMBOL | FIXED_SYMBOL) floatOptions? fieldOptions?
| type = BIT_SYMBOL fieldLength?
| type = (BOOL_SYMBOL | BOOLEAN_SYMBOL)
- /* @FIX: Moved "CHAR_SYMBOL VARYING_SYMBOL" before "CHAR_SYMBOL ..." to solve conflict. */
+ /* @CHANGED: Moved "CHAR_SYMBOL VARYING_SYMBOL" before "CHAR_SYMBOL ..." to solve conflict. */
| (type = CHAR_SYMBOL VARYING_SYMBOL | type = VARCHAR_SYMBOL) fieldLength charsetWithOptBinary?
| type = CHAR_SYMBOL fieldLength? charsetWithOptBinary?
/*| nchar fieldLength? BINARY_SYMBOL? */
@@ -3836,7 +3836,7 @@ dataType: // type in sql_yacc.yy
| type = NATIONAL_SYMBOL CHAR_SYMBOL VARYING_SYMBOL
| type = NCHAR_SYMBOL VARYING_SYMBOL
) fieldLength BINARY_SYMBOL?
- /* @FIX: Moved "nchar fieldLength? BINARY_SYMBOL?" after othe nchar definitions to solve conflicts. */
+ /* @CHANGED: Moved "nchar fieldLength? BINARY_SYMBOL?" after othe nchar definitions to solve conflicts. */
| nchar fieldLength? BINARY_SYMBOL?
| type = VARBINARY_SYMBOL fieldLength
| type = YEAR_SYMBOL fieldLength? fieldOptions?
@@ -3962,7 +3962,7 @@ createTableOption: // In the order as they appear in the server grammar.
| REDUNDANT_SYMBOL
| COMPACT_SYMBOL
)
- /* @FIX: Make "tablRefList" optional. */
+ /* @CHANGED: Make "tablRefList" optional. */
| option = UNION_SYMBOL EQUAL_OPERATOR? OPEN_PAR_SYMBOL tableRefList? CLOSE_PAR_SYMBOL
| defaultCharset
| defaultCollation
@@ -3980,7 +3980,7 @@ createTableOption: // In the order as they appear in the server grammar.
| option = STORAGE_SYMBOL (DISK_SYMBOL | MEMORY_SYMBOL)
| option = CONNECTION_SYMBOL EQUAL_OPERATOR? textString
| option = KEY_BLOCK_SIZE_SYMBOL EQUAL_OPERATOR? ulong_number
- /* @FIX: Add missing options. */
+ /* @CHANGED: Add missing options. */
| {serverVersion >= 80021}? option = START_SYMBOL TRANSACTION_SYMBOL
| {serverVersion >= 80021}? option = ENGINE_ATTRIBUTE_SYMBOL EQUAL_OPERATOR? textString
| {serverVersion >= 80021}? option = SECONDARY_ENGINE_ATTRIBUTE_SYMBOL EQUAL_OPERATOR? textString
@@ -4119,7 +4119,7 @@ updateList:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Change "EQUAL_OPERATOR" to "equal" to add support for ":=".
*/
updateElement:
@@ -4187,7 +4187,7 @@ createUserEntry: // create_user in sql_yacc.yy
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix "alterUserEntry":
* 1. Support also "USER()" function call.
* 2. Add support for "RANDOM PASSWORD".
@@ -4221,7 +4221,7 @@ replacePassword:
;*/
/*
- * @FIX:
+ * @CHANGED:
* Fix "userIdentifierOrText" to support omitting sequence after "@".
*/
userIdentifierOrText:
@@ -4597,7 +4597,7 @@ nullLiteral: // In sql_yacc.cc both 'NULL' and '\N' are mapped to NULL_SYM (whic
;*/
/*
- * @FIX:
+ * @CHANGED:
* Replace "SINGLE_QUOTED_TEXT" with "textStringLiteral" to support both ' and ", as per SQL_MODE.
*/
temporalLiteral:
From f7c99c1340cf486986b57530b23e206d9fcb8f1b Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 5 Feb 2025 14:10:29 +0100
Subject: [PATCH 109/124] Improve parser node method naming
---
tests/parser/WP_Parser_Node_Tests.php | 40 ++---
wp-includes/parser/class-wp-parser-node.php | 14 +-
.../sqlite-ast/class-wp-sqlite-driver.php | 122 +++++++--------
...s-wp-sqlite-information-schema-builder.php | 140 +++++++++---------
4 files changed, 158 insertions(+), 158 deletions(-)
diff --git a/tests/parser/WP_Parser_Node_Tests.php b/tests/parser/WP_Parser_Node_Tests.php
index e500f83..6f01d21 100644
--- a/tests/parser/WP_Parser_Node_Tests.php
+++ b/tests/parser/WP_Parser_Node_Tests.php
@@ -12,14 +12,14 @@ public function testEmptyChildren(): void {
$this->assertFalse( $node->has_child_node() );
$this->assertFalse( $node->has_child_token() );
- $this->assertNull( $node->get_child() );
- $this->assertNull( $node->get_child_node() );
- $this->assertNull( $node->get_child_node( 'root' ) );
- $this->assertNull( $node->get_child_token() );
- $this->assertNull( $node->get_child_token( 1 ) );
+ $this->assertNull( $node->get_first_child() );
+ $this->assertNull( $node->get_first_child_node() );
+ $this->assertNull( $node->get_first_child_node( 'root' ) );
+ $this->assertNull( $node->get_first_child_token() );
+ $this->assertNull( $node->get_first_child_token( 1 ) );
- $this->assertNull( $node->get_descendant_node() );
- $this->assertNull( $node->get_descendant_token() );
+ $this->assertNull( $node->get_first_descendant_node() );
+ $this->assertNull( $node->get_first_descendant_token() );
$this->assertEmpty( $node->get_children() );
$this->assertEmpty( $node->get_child_nodes() );
@@ -84,13 +84,13 @@ public function testNodeTree(): void {
$this->assertTrue( $root->has_child_token() );
// Test single child methods.
- $this->assertSame( $n_keyword, $root->get_child() );
- $this->assertSame( $n_keyword, $root->get_child_node() );
- $this->assertSame( $n_keyword, $root->get_child_node( 'keyword' ) );
- $this->assertSame( $n_expr_a, $root->get_child_node( 'expr' ) );
- $this->assertSame( $t_comma, $root->get_child_token() );
- $this->assertSame( $t_comma, $root->get_child_token( 200 ) );
- $this->assertNull( $root->get_child_token( 100 ) );
+ $this->assertSame( $n_keyword, $root->get_first_child() );
+ $this->assertSame( $n_keyword, $root->get_first_child_node() );
+ $this->assertSame( $n_keyword, $root->get_first_child_node( 'keyword' ) );
+ $this->assertSame( $n_expr_a, $root->get_first_child_node( 'expr' ) );
+ $this->assertSame( $t_comma, $root->get_first_child_token() );
+ $this->assertSame( $t_comma, $root->get_first_child_token( 200 ) );
+ $this->assertNull( $root->get_first_child_token( 100 ) );
// Test multiple children methods.
$this->assertSame( array( $n_keyword, $n_expr_a, $t_comma, $n_expr_b, $t_eof ), $root->get_children() );
@@ -103,12 +103,12 @@ public function testNodeTree(): void {
// Test single descendant methods.
// @TODO: Consider breadth-first search vs depth-first search.
- $this->assertSame( $n_keyword, $root->get_descendant_node() );
- $this->assertSame( $n_expr_a, $root->get_descendant_node( 'expr' ) );
- $this->assertSame( null, $root->get_descendant_node( 'root' ) );
- $this->assertSame( $t_comma, $root->get_descendant_token() );
- $this->assertSame( $t_one, $root->get_descendant_token( 400 ) );
- $this->assertSame( null, $root->get_descendant_token( 123 ) );
+ $this->assertSame( $n_keyword, $root->get_first_descendant_node() );
+ $this->assertSame( $n_expr_a, $root->get_first_descendant_node( 'expr' ) );
+ $this->assertSame( null, $root->get_first_descendant_node( 'root' ) );
+ $this->assertSame( $t_comma, $root->get_first_descendant_token() );
+ $this->assertSame( $t_one, $root->get_first_descendant_token( 400 ) );
+ $this->assertSame( null, $root->get_first_descendant_token( 123 ) );
// Test multiple descendant methods.
// @TODO: Consider breadth-first search vs depth-first search.
diff --git a/wp-includes/parser/class-wp-parser-node.php b/wp-includes/parser/class-wp-parser-node.php
index 81ea137..9d66233 100644
--- a/wp-includes/parser/class-wp-parser-node.php
+++ b/wp-includes/parser/class-wp-parser-node.php
@@ -131,11 +131,11 @@ public function has_child_token( ?int $token_id = null ): bool {
}
- public function get_child() {
+ public function get_first_child() {
return $this->children[0] ?? null;
}
- public function get_child_node( ?string $rule_name = null ): ?WP_Parser_Node {
+ public function get_first_child_node( ?string $rule_name = null ): ?WP_Parser_Node {
foreach ( $this->children as $child ) {
if (
$child instanceof WP_Parser_Node
@@ -147,7 +147,7 @@ public function get_child_node( ?string $rule_name = null ): ?WP_Parser_Node {
return null;
}
- public function get_child_token( ?int $token_id = null ): ?WP_Parser_Token {
+ public function get_first_child_token( ?int $token_id = null ): ?WP_Parser_Token {
foreach ( $this->children as $child ) {
if (
$child instanceof WP_Parser_Token
@@ -159,11 +159,11 @@ public function get_child_token( ?int $token_id = null ): ?WP_Parser_Token {
return null;
}
- public function get_descendant_node( ?string $rule_name = null ): ?WP_Parser_Node {
+ public function get_first_descendant_node( ?string $rule_name = null ): ?WP_Parser_Node {
$nodes = array( $this );
while ( count( $nodes ) ) {
$node = array_shift( $nodes );
- $child = $node->get_child_node( $rule_name );
+ $child = $node->get_first_child_node( $rule_name );
if ( $child ) {
return $child;
}
@@ -175,11 +175,11 @@ public function get_descendant_node( ?string $rule_name = null ): ?WP_Parser_Nod
return null;
}
- public function get_descendant_token( ?int $token_id = null ): ?WP_Parser_Token {
+ public function get_first_descendant_token( ?int $token_id = null ): ?WP_Parser_Token {
$nodes = array( $this );
while ( count( $nodes ) ) {
$node = array_shift( $nodes );
- $child = $node->get_child_token( $token_id );
+ $child = $node->get_first_child_token( $token_id );
if ( $child ) {
return $child;
}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index f858bab..60cf426 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -453,13 +453,13 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
}
// Handle transaction commands.
- $child = $ast->get_child();
+ $child = $ast->get_first_child();
if ( $child instanceof WP_Parser_Node && 'beginWork' === $child->rule_name ) {
return $this->begin_transaction();
}
if ( $child instanceof WP_Parser_Node && 'simpleStatement' === $child->rule_name ) {
- $subchild = $child->get_child_node( 'transactionOrLockingStatement' );
+ $subchild = $child->get_first_child_node( 'transactionOrLockingStatement' );
if ( null !== $subchild ) {
$tokens = $subchild->get_descendant_tokens();
$token1 = $tokens[0];
@@ -665,7 +665,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
throw new Exception( sprintf( 'Expected 1 child, got: %d', count( $children ) ) );
}
- $ast = $children[0]->get_child_node();
+ $ast = $children[0]->get_first_child_node();
switch ( $ast->rule_name ) {
case 'selectStatement':
$this->execute_select_statement( $ast );
@@ -681,7 +681,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$this->execute_delete_statement( $ast );
break;
case 'createStatement':
- $subtree = $ast->get_child_node();
+ $subtree = $ast->get_first_child_node();
switch ( $subtree->rule_name ) {
case 'createTable':
$this->execute_create_table_statement( $ast );
@@ -697,7 +697,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
}
break;
case 'alterStatement':
- $subtree = $ast->get_child_node();
+ $subtree = $ast->get_first_child_node();
switch ( $subtree->rule_name ) {
case 'alterTable':
$this->execute_alter_table_statement( $ast );
@@ -713,7 +713,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
}
break;
case 'dropStatement':
- $subtree = $ast->get_child_node();
+ $subtree = $ast->get_first_child_node();
switch ( $subtree->rule_name ) {
case 'dropTable':
$this->execute_drop_table_statement( $ast );
@@ -735,7 +735,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$this->execute_show_statement( $ast );
break;
case 'utilityStatement':
- $subtree = $ast->get_child_node();
+ $subtree = $ast->get_first_child_node();
switch ( $subtree->rule_name ) {
case 'describeStatement':
$this->execute_describe_statement( $subtree );
@@ -756,39 +756,39 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
}
private function execute_select_statement( WP_Parser_Node $node ): void {
- $has_sql_calc_found_rows = null !== $node->get_descendant_token(
+ $has_sql_calc_found_rows = null !== $node->get_first_descendant_token(
WP_MySQL_Lexer::SQL_CALC_FOUND_ROWS_SYMBOL
);
// First, translate the query, before we modify last found rows count.
- $query = $this->translate( $node->get_child() );
+ $query = $this->translate( $node->get_first_child() );
// Handle SQL_CALC_FOUND_ROWS.
if ( true === $has_sql_calc_found_rows ) {
// Recursively find a query expression with the first LIMIT or SELECT.
- $query_expr = $node->get_descendant_node( 'queryExpression' );
+ $query_expr = $node->get_first_descendant_node( 'queryExpression' );
while ( true ) {
if ( $query_expr->has_child_node( 'limitClause' ) ) {
break;
}
- $query_expr_parens = $query_expr->get_child_node( 'queryExpressionParens' );
+ $query_expr_parens = $query_expr->get_first_child_node( 'queryExpressionParens' );
if ( null !== $query_expr_parens ) {
- $query_expr = $query_expr_parens->get_child_node( 'queryExpression' );
+ $query_expr = $query_expr_parens->get_first_child_node( 'queryExpression' );
continue;
}
- $query_expr_body = $query_expr->get_child_node( 'queryExpressionBody' );
+ $query_expr_body = $query_expr->get_first_child_node( 'queryExpressionBody' );
if ( count( $query_expr_body->get_children() ) > 1 ) {
break;
}
- $query_term = $query_expr_body->get_child_node( 'queryTerm' );
+ $query_term = $query_expr_body->get_first_child_node( 'queryTerm' );
if (
count( $query_term->get_children() ) === 1
&& $query_term->has_child_node( 'queryExpressionParens' )
) {
- $query_expr = $query_term->get_child_node( 'queryExpressionParens' )->get_child_node( 'queryExpression' );
+ $query_expr = $query_term->get_first_child_node( 'queryExpressionParens' )->get_first_child_node( 'queryExpression' );
continue;
}
@@ -849,10 +849,10 @@ private function execute_update_statement( WP_Parser_Node $node ): void {
if ( $has_order || $has_limit ) {
$where_subquery = 'SELECT rowid FROM ' . $this->translate_sequence(
array(
- $node->get_child_node( 'tableReferenceList' ),
- $node->get_child_node( 'whereClause' ),
- $node->get_child_node( 'orderClause' ),
- $node->get_child_node( 'simpleLimitClause' ),
+ $node->get_first_child_node( 'tableReferenceList' ),
+ $node->get_first_child_node( 'whereClause' ),
+ $node->get_first_child_node( 'orderClause' ),
+ $node->get_first_child_node( 'simpleLimitClause' ),
)
);
}
@@ -899,7 +899,7 @@ private function execute_delete_statement( WP_Parser_Node $node ): void {
* We will rewrite such statements into a SELECT to fetch the ROWIDs of
* the rows to delete and then execute a DELETE statement for each table.
*/
- $alias_ref_list = $node->get_child_node( 'tableAliasRefList' );
+ $alias_ref_list = $node->get_first_child_node( 'tableAliasRefList' );
if ( null !== $alias_ref_list ) {
// 1. Get table aliases targeted by the DELETE statement.
$table_aliases = array();
@@ -911,22 +911,22 @@ private function execute_delete_statement( WP_Parser_Node $node ): void {
// 2. Create an alias to table name map.
$alias_map = array();
- $table_ref_list = $node->get_child_node( 'tableReferenceList' );
+ $table_ref_list = $node->get_first_child_node( 'tableReferenceList' );
foreach ( $table_ref_list->get_descendant_nodes( 'singleTable' ) as $single_table ) {
$alias = $this->unquote_sqlite_identifier(
- $this->translate( $single_table->get_child_node( 'tableAlias' ) )
+ $this->translate( $single_table->get_first_child_node( 'tableAlias' ) )
);
$ref = $this->unquote_sqlite_identifier(
- $this->translate( $single_table->get_child_node( 'tableRef' ) )
+ $this->translate( $single_table->get_first_child_node( 'tableRef' ) )
);
$alias_map[ $alias ] = $ref;
}
// 3. Compose the SELECT query to fetch ROWIDs to delete.
- $where_clause = $node->get_child_node( 'whereClause' );
+ $where_clause = $node->get_first_child_node( 'whereClause' );
if ( null !== $where_clause ) {
- $where = $this->translate( $where_clause->get_child_node( 'expr' ) );
+ $where = $this->translate( $where_clause->get_first_child_node( 'expr' ) );
}
$select_list = array();
@@ -972,9 +972,9 @@ private function execute_delete_statement( WP_Parser_Node $node ): void {
}
private function execute_create_table_statement( WP_Parser_Node $node ): void {
- $subnode = $node->get_child_node();
+ $subnode = $node->get_first_child_node();
$is_temporary = $subnode->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL );
- $element_list = $subnode->get_child_node( 'tableElementList' );
+ $element_list = $subnode->get_first_child_node( 'tableElementList' );
if ( true === $is_temporary || null === $element_list ) {
$query = $this->translate( $node ) . ' STRICT';
$this->execute_sqlite_query( $query );
@@ -983,7 +983,7 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void {
}
$table_name = $this->unquote_sqlite_identifier(
- $this->translate( $subnode->get_child_node( 'tableName' ) )
+ $this->translate( $subnode->get_first_child_node( 'tableName' ) )
);
if ( $subnode->has_child_node( 'ifNotExists' ) ) {
@@ -1015,7 +1015,7 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void {
private function execute_alter_table_statement( WP_Parser_Node $node ): void {
$table_name = $this->unquote_sqlite_identifier(
- $this->translate( $node->get_descendant_node( 'tableRef' ) )
+ $this->translate( $node->get_first_descendant_node( 'tableRef' ) )
);
// Save all column names from the original table.
@@ -1032,11 +1032,11 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void {
// Track column renames and removals.
$column_map = array_combine( $column_names, $column_names );
foreach ( $node->get_descendant_nodes( 'alterListItem' ) as $action ) {
- $first_token = $action->get_child_token();
+ $first_token = $action->get_first_child_token();
switch ( $first_token->id ) {
case WP_MySQL_Lexer::DROP_SYMBOL:
- $name = $this->translate( $action->get_child_node( 'columnInternalRef' ) );
+ $name = $this->translate( $action->get_first_child_node( 'columnInternalRef' ) );
if ( null !== $name ) {
$name = $this->unquote_sqlite_identifier( $name );
unset( $column_map[ $name ] );
@@ -1044,22 +1044,22 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void {
break;
case WP_MySQL_Lexer::CHANGE_SYMBOL:
$old_name = $this->unquote_sqlite_identifier(
- $this->translate( $action->get_child_node( 'columnInternalRef' ) )
+ $this->translate( $action->get_first_child_node( 'columnInternalRef' ) )
);
$new_name = $this->unquote_sqlite_identifier(
- $this->translate( $action->get_child_node( 'identifier' ) )
+ $this->translate( $action->get_first_child_node( 'identifier' ) )
);
$column_map[ $old_name ] = $new_name;
break;
case WP_MySQL_Lexer::RENAME_SYMBOL:
- $column_ref = $action->get_child_node( 'columnInternalRef' );
+ $column_ref = $action->get_first_child_node( 'columnInternalRef' );
if ( null !== $column_ref ) {
$old_name = $this->unquote_sqlite_identifier(
$this->translate( $column_ref )
);
$new_name = $this->unquote_sqlite_identifier(
- $this->translate( $action->get_child_node( 'identifier' ) )
+ $this->translate( $action->get_first_child_node( 'identifier' ) )
);
$column_map[ $old_name ] = $new_name;
@@ -1135,11 +1135,11 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void {
}
private function execute_drop_table_statement( WP_Parser_Node $node ): void {
- $child_node = $node->get_child_node();
+ $child_node = $node->get_first_child_node();
// MySQL supports removing multiple tables in a single query DROP query.
// In SQLite, we need to execute each DROP TABLE statement separately.
- $table_refs = $child_node->get_child_node( 'tableRefList' )->get_child_nodes();
+ $table_refs = $child_node->get_first_child_node( 'tableRefList' )->get_child_nodes();
$is_temporary = $child_node->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL );
$queries = array();
foreach ( $table_refs as $table_ref ) {
@@ -1183,7 +1183,7 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
case WP_MySQL_Lexer::CREATE_SYMBOL:
if ( WP_MySQL_Lexer::TABLE_SYMBOL === $keyword2->id ) {
$table_name = $this->unquote_sqlite_identifier(
- $this->translate( $node->get_child_node( 'tableRef' ) )
+ $this->translate( $node->get_first_child_node( 'tableRef' ) )
);
$sql = $this->get_mysql_create_table_statement( $table_name );
@@ -1205,7 +1205,7 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
case WP_MySQL_Lexer::INDEXES_SYMBOL:
case WP_MySQL_Lexer::KEYS_SYMBOL:
$table_name = $this->unquote_sqlite_identifier(
- $this->translate( $node->get_child_node( 'tableRef' ) )
+ $this->translate( $node->get_first_child_node( 'tableRef' ) )
);
$this->execute_show_index_statement( $table_name );
break;
@@ -1273,11 +1273,11 @@ private function execute_show_index_statement( string $table_name ): void {
private function execute_show_table_status_statement( WP_Parser_Node $node ): void {
// FROM/IN database.
- $in_db = $node->get_child_node( 'idDb' );
+ $in_db = $node->get_first_child_node( 'idDb' );
$database = null === $in_db ? $this->db_name : $this->translate( $in_db );
// LIKE and WHERE clauses.
- $like_or_where = $node->get_child_node( 'likeOrWhere' );
+ $like_or_where = $node->get_first_child_node( 'likeOrWhere' );
if ( null !== $like_or_where ) {
$condition = $this->get_show_like_or_where_condition( $like_or_where );
}
@@ -1325,11 +1325,11 @@ private function execute_show_table_status_statement( WP_Parser_Node $node ): vo
private function execute_show_tables_statement( WP_Parser_Node $node ): void {
// FROM/IN database.
- $in_db = $node->get_child_node( 'idDb' );
+ $in_db = $node->get_first_child_node( 'idDb' );
$database = null === $in_db ? $this->db_name : $this->translate( $in_db );
// LIKE and WHERE clauses.
- $like_or_where = $node->get_child_node( 'likeOrWhere' );
+ $like_or_where = $node->get_first_child_node( 'likeOrWhere' );
if ( null !== $like_or_where ) {
$condition = $this->get_show_like_or_where_condition( $like_or_where );
}
@@ -1348,7 +1348,7 @@ private function execute_show_tables_statement( WP_Parser_Node $node ): void {
}
// Handle the FULL keyword.
- $command_type = $node->get_child_node( 'showCommandType' );
+ $command_type = $node->get_first_child_node( 'showCommandType' );
$is_full = $command_type && $command_type->has_child_token( WP_MySQL_Lexer::FULL_SYMBOL );
// Format the results.
@@ -1368,7 +1368,7 @@ private function execute_show_tables_statement( WP_Parser_Node $node ): void {
private function execute_describe_statement( WP_Parser_Node $node ): void {
$table_name = $this->unquote_sqlite_identifier(
- $this->translate( $node->get_child_node( 'tableRef' ) )
+ $this->translate( $node->get_first_child_node( 'tableRef' ) )
);
$column_info = $this->execute_sqlite_query(
@@ -1425,14 +1425,14 @@ private function translate( $ast ) {
case 'dotIdentifier':
return $this->translate_sequence( $ast->get_children(), '' );
case 'identifierKeyword':
- return '`' . $this->translate( $ast->get_child() ) . '`';
+ return '`' . $this->translate( $ast->get_first_child() ) . '`';
case 'pureIdentifier':
return $this->translate_pure_identifier( $ast );
case 'textStringLiteral':
return $this->translate_string_literal( $ast );
case 'dataType':
case 'nchar':
- $child = $ast->get_child();
+ $child = $ast->get_first_child();
if ( $child instanceof WP_Parser_Node ) {
return $this->translate( $child );
}
@@ -1475,12 +1475,12 @@ private function translate( $ast ) {
// Translate "ON DUPLICATE KEY UPDATE" to "ON CONFLICT DO UPDATE SET".
return sprintf(
'ON CONFLICT DO UPDATE SET %s',
- $this->translate( $ast->get_child_node( 'updateList' ) )
+ $this->translate( $ast->get_first_child_node( 'updateList' ) )
);
case 'simpleExpr':
return $this->translate_simple_expr( $ast );
case 'predicateOperations':
- $token = $ast->get_child_token();
+ $token = $ast->get_first_child_token();
if ( WP_MySQL_Lexer::LIKE_SYMBOL === $token->id ) {
return $this->translate_like( $ast );
} elseif ( WP_MySQL_Lexer::REGEXP_SYMBOL === $token->id ) {
@@ -1511,7 +1511,7 @@ private function translate( $ast ) {
// @TODO: How to handle IGNORE/REPLACE?
// The "AS" keyword is optional in MySQL, but required in SQLite.
- return 'AS ' . $this->translate( $ast->get_child_node() );
+ return 'AS ' . $this->translate( $ast->get_first_child_node() );
case 'indexHint':
case 'indexHintList':
return null;
@@ -1564,7 +1564,7 @@ private function translate_sequence( array $nodes, string $separator = ' ' ): ?s
}
private function translate_string_literal( WP_Parser_Node $node ): string {
- $token = $node->get_child_token();
+ $token = $node->get_first_child_token();
/*
* 1. Remove bounding quotes.
@@ -1647,7 +1647,7 @@ private function translate_string_literal( WP_Parser_Node $node ): string {
}
private function translate_pure_identifier( WP_Parser_Node $node ): string {
- $token = $node->get_child_token();
+ $token = $node->get_first_child_token();
if ( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT === $token->id ) {
$value = substr( $token->value, 1, -1 );
@@ -1663,13 +1663,13 @@ private function translate_pure_identifier( WP_Parser_Node $node ): string {
}
private function translate_simple_expr( WP_Parser_Node $node ): string {
- $token = $node->get_child_token();
+ $token = $node->get_first_child_token();
// Translate "VALUES(col)" to "excluded.col" in ON DUPLICATE KEY UPDATE.
if ( null !== $token && WP_MySQL_Lexer::VALUES_SYMBOL === $token->id ) {
return sprintf(
'`excluded`.%s',
- $this->translate( $node->get_child_node( 'simpleIdentifier' ) )
+ $this->translate( $node->get_first_child_node( 'simpleIdentifier' ) )
);
}
@@ -1727,13 +1727,13 @@ private function translate_regexp_functions( WP_Parser_Node $node ): string {
* regular expressions anyway.
*/
if ( true === $is_binary ) {
- return 'REGEXP CHAR(0) || ' . $this->translate( $node->get_child_node() );
+ return 'REGEXP CHAR(0) || ' . $this->translate( $node->get_first_child_node() );
}
- return 'REGEXP ' . $this->translate( $node->get_child_node() );
+ return 'REGEXP ' . $this->translate( $node->get_first_child_node() );
}
private function translate_runtime_function_call( WP_Parser_Node $node ): string {
- $child = $node->get_child();
+ $child = $node->get_first_child();
if ( $child instanceof WP_Parser_Node ) {
return $this->translate( $child );
}
@@ -2201,18 +2201,18 @@ function ( $column ) {
}
private function get_show_like_or_where_condition( WP_Parser_Node $like_or_where ): string {
- $like_clause = $like_or_where->get_child_node( 'likeClause' );
+ $like_clause = $like_or_where->get_first_child_node( 'likeClause' );
if ( null !== $like_clause ) {
$value = $this->translate(
- $like_clause->get_child_node( 'textStringLiteral' )
+ $like_clause->get_first_child_node( 'textStringLiteral' )
);
return sprintf( "AND table_name LIKE %s ESCAPE '\\'", $value );
}
- $where_clause = $like_or_where->get_child_node( 'whereClause' );
+ $where_clause = $like_or_where->get_first_child_node( 'whereClause' );
if ( null !== $where_clause ) {
$value = $this->translate(
- $where_clause->get_child_node( 'expr' )
+ $where_clause->get_first_child_node( 'expr' )
);
return sprintf( 'AND %s', $value );
}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index 271cf55..a0e13e1 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -359,7 +359,7 @@ public function ensure_information_schema_tables(): void {
* @param WP_Parser_Node $node AST node representing a CREATE TABLE statement.
*/
public function record_create_table( WP_Parser_Node $node ): void {
- $table_name = $this->get_value( $node->get_descendant_node( 'tableName' ) );
+ $table_name = $this->get_value( $node->get_first_descendant_node( 'tableName' ) );
$table_engine = $this->get_table_engine( $node );
$table_row_format = 'MyISAM' === $table_engine ? 'FIXED' : 'DYNAMIC';
$table_collation = $this->get_table_collation( $node );
@@ -380,7 +380,7 @@ public function record_create_table( WP_Parser_Node $node ): void {
// 2. Columns.
$column_position = 1;
foreach ( $node->get_descendant_nodes( 'columnDefinition' ) as $column_node ) {
- $column_name = $this->get_value( $column_node->get_child_node( 'columnName' ) );
+ $column_name = $this->get_value( $column_node->get_first_child_node( 'columnName' ) );
// Column definition.
$column_data = $this->extract_column_data(
@@ -415,11 +415,11 @@ public function record_create_table( WP_Parser_Node $node ): void {
}
public function record_alter_table( WP_Parser_Node $node ): void {
- $table_name = $this->get_value( $node->get_descendant_node( 'tableRef' ) );
+ $table_name = $this->get_value( $node->get_first_descendant_node( 'tableRef' ) );
$actions = $node->get_descendant_nodes( 'alterListItem' );
foreach ( $actions as $action ) {
- $first_token = $action->get_child_token();
+ $first_token = $action->get_first_child_token();
// ADD
if ( WP_MySQL_Lexer::ADD_SYMBOL === $first_token->id ) {
@@ -427,23 +427,23 @@ public function record_alter_table( WP_Parser_Node $node ): void {
$column_definitions = $action->get_descendant_nodes( 'columnDefinition' );
if ( count( $column_definitions ) > 0 ) {
foreach ( $column_definitions as $column_definition ) {
- $name = $this->get_value( $column_definition->get_child_node( 'identifier' ) );
+ $name = $this->get_value( $column_definition->get_first_child_node( 'identifier' ) );
$this->record_add_column( $table_name, $name, $column_definition );
}
continue;
}
// ADD [COLUMN] ...
- $field_definition = $action->get_descendant_node( 'fieldDefinition' );
+ $field_definition = $action->get_first_descendant_node( 'fieldDefinition' );
if ( null !== $field_definition ) {
- $name = $this->get_value( $action->get_child_node( 'identifier' ) );
+ $name = $this->get_value( $action->get_first_child_node( 'identifier' ) );
$this->record_add_column( $table_name, $name, $field_definition );
// @TODO: Handle FIRST/AFTER.
continue;
}
// ADD CONSTRAINT.
- $constraint = $action->get_descendant_node( 'tableConstraintDef' );
+ $constraint = $action->get_first_descendant_node( 'tableConstraintDef' );
if ( null !== $constraint ) {
$this->record_add_constraint( $table_name, $constraint );
continue;
@@ -454,24 +454,24 @@ public function record_alter_table( WP_Parser_Node $node ): void {
// CHANGE [COLUMN]
if ( WP_MySQL_Lexer::CHANGE_SYMBOL === $first_token->id ) {
- $old_name = $this->get_value( $action->get_child_node( 'columnInternalRef' ) );
- $new_name = $this->get_value( $action->get_child_node( 'identifier' ) );
+ $old_name = $this->get_value( $action->get_first_child_node( 'columnInternalRef' ) );
+ $new_name = $this->get_value( $action->get_first_child_node( 'identifier' ) );
$this->record_change_column(
$table_name,
$old_name,
$new_name,
- $action->get_descendant_node( 'fieldDefinition' )
+ $action->get_first_descendant_node( 'fieldDefinition' )
);
continue;
}
// MODIFY [COLUMN]
if ( WP_MySQL_Lexer::MODIFY_SYMBOL === $first_token->id ) {
- $name = $this->get_value( $action->get_child_node( 'columnInternalRef' ) );
+ $name = $this->get_value( $action->get_first_child_node( 'columnInternalRef' ) );
$this->record_modify_column(
$table_name,
$name,
- $action->get_descendant_node( 'fieldDefinition' )
+ $action->get_first_descendant_node( 'fieldDefinition' )
);
continue;
}
@@ -479,7 +479,7 @@ public function record_alter_table( WP_Parser_Node $node ): void {
// DROP
if ( WP_MySQL_Lexer::DROP_SYMBOL === $first_token->id ) {
// DROP [COLUMN]
- $column_ref = $action->get_child_node( 'columnInternalRef' );
+ $column_ref = $action->get_first_child_node( 'columnInternalRef' );
if ( null !== $column_ref ) {
$name = $this->get_value( $column_ref );
$this->record_drop_column( $table_name, $name );
@@ -488,7 +488,7 @@ public function record_alter_table( WP_Parser_Node $node ): void {
// DROP INDEX
if ( $action->has_child_node( 'keyOrIndex' ) ) {
- $name = $this->get_value( $action->get_child_node( 'indexRef' ) );
+ $name = $this->get_value( $action->get_first_child_node( 'indexRef' ) );
$this->record_drop_index( $table_name, $name );
continue;
}
@@ -497,12 +497,12 @@ public function record_alter_table( WP_Parser_Node $node ): void {
}
public function record_drop_table( WP_Parser_Node $node ): void {
- $child_node = $node->get_child_node();
+ $child_node = $node->get_first_child_node();
if ( $child_node->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL ) ) {
return;
}
- $table_refs = $child_node->get_child_node( 'tableRefList' )->get_child_nodes();
+ $table_refs = $child_node->get_first_child_node( 'tableRefList' )->get_child_nodes();
foreach ( $table_refs as $table_ref ) {
$table_name = $this->get_value( $table_ref );
$this->delete_values(
@@ -659,7 +659,7 @@ private function record_add_constraint( string $table_name, WP_Parser_Node $node
$children = $node->get_children();
$keyword = $children[0] instanceof WP_MySQL_Token ? $children[0] : $children[1];
if ( ! $keyword instanceof WP_MySQL_Token ) {
- $keyword = $keyword->get_child_token();
+ $keyword = $keyword->get_first_child_token();
}
if (
@@ -670,11 +670,11 @@ private function record_add_constraint( string $table_name, WP_Parser_Node $node
}
// Get key parts.
- $key_list = $node->get_child_node( 'keyListVariants' )->get_child();
+ $key_list = $node->get_first_child_node( 'keyListVariants' )->get_first_child();
if ( 'keyListWithExpression' === $key_list->rule_name ) {
$key_parts = array();
foreach ( $key_list->get_descendant_nodes( 'keyPartOrExpression' ) as $key_part ) {
- $key_parts[] = $key_part->get_child();
+ $key_parts[] = $key_part->get_first_child();
}
} else {
$key_parts = $key_list->get_descendant_nodes( 'keyPart' );
@@ -808,8 +808,8 @@ private function extract_column_data( string $table_name, string $column_name, W
private function extract_column_constraint_data( string $table_name, string $column_name, WP_Parser_Node $node, bool $nullable ): ?array {
// Handle inline PRIMARY KEY and UNIQUE constraints.
- $has_inline_primary_key = null !== $node->get_descendant_token( WP_MySQL_Lexer::KEY_SYMBOL );
- $has_inline_unique_key = null !== $node->get_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL );
+ $has_inline_primary_key = null !== $node->get_first_descendant_token( WP_MySQL_Lexer::KEY_SYMBOL );
+ $has_inline_unique_key = null !== $node->get_first_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL );
if ( $has_inline_primary_key || $has_inline_unique_key ) {
$index_name = $has_inline_primary_key ? 'PRIMARY' : $column_name;
return array(
@@ -881,7 +881,7 @@ private function sync_column_key_info( string $table_name ): void {
}
private function get_table_engine( WP_Parser_Node $node ): string {
- $engine_node = $node->get_descendant_node( 'engineRef' );
+ $engine_node = $node->get_first_descendant_node( 'engineRef' );
if ( null === $engine_node ) {
return 'InnoDB';
}
@@ -896,7 +896,7 @@ private function get_table_engine( WP_Parser_Node $node ): string {
}
private function get_table_collation( WP_Parser_Node $node ): string {
- $collate_node = $node->get_descendant_node( 'collationName' );
+ $collate_node = $node->get_first_descendant_node( 'collationName' );
if ( null === $collate_node ) {
// @TODO: Use default DB collation or DB_CHARSET & DB_COLLATE.
return 'utf8mb4_general_ci';
@@ -922,20 +922,20 @@ private function get_column_default( WP_Parser_Node $node ): ?string {
if (
$default_attr->has_child_node( 'signedLiteral' )
- && null !== $default_attr->get_descendant_node( 'nullLiteral' )
+ && null !== $default_attr->get_first_descendant_node( 'nullLiteral' )
) {
return null;
}
// @TODO: MySQL seems to normalize default values for numeric
// columns, such as 1.0 to 1, 1e3 to 1000, etc.
- return $this->get_value( $default_attr->get_child_node() );
+ return $this->get_value( $default_attr->get_first_child_node() );
}
private function get_column_nullable( WP_Parser_Node $node ): string {
// SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
- $data_type = $node->get_descendant_node( 'dataType' );
- if ( null !== $data_type->get_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) {
+ $data_type = $node->get_first_descendant_node( 'dataType' );
+ if ( null !== $data_type->get_first_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) {
return 'NO';
}
@@ -959,24 +959,24 @@ private function get_column_nullable( WP_Parser_Node $node ): string {
private function get_column_key( WP_Parser_Node $column_node ): string {
// 1. PRI: Column is a primary key or its any component.
if (
- null !== $column_node->get_descendant_token( WP_MySQL_Lexer::KEY_SYMBOL )
+ null !== $column_node->get_first_descendant_token( WP_MySQL_Lexer::KEY_SYMBOL )
) {
return 'PRI';
}
// SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
- $data_type = $column_node->get_descendant_node( 'dataType' );
- if ( null !== $data_type->get_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) {
+ $data_type = $column_node->get_first_descendant_node( 'dataType' );
+ if ( null !== $data_type->get_first_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) {
return 'PRI';
}
// 2. UNI: Column has UNIQUE constraint.
- if ( null !== $column_node->get_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ) ) {
+ if ( null !== $column_node->get_first_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ) ) {
return 'UNI';
}
// 3. MUL: Column has INDEX.
- if ( null !== $column_node->get_descendant_token( WP_MySQL_Lexer::INDEX_SYMBOL ) ) {
+ if ( null !== $column_node->get_first_descendant_token( WP_MySQL_Lexer::INDEX_SYMBOL ) ) {
return 'MUL';
}
@@ -988,8 +988,8 @@ private function get_column_extra( WP_Parser_Node $node ): string {
$attributes = $node->get_descendant_nodes( 'columnAttribute' );
// SERIAL
- $data_type = $node->get_descendant_node( 'dataType' );
- if ( null !== $data_type->get_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) {
+ $data_type = $node->get_first_descendant_node( 'dataType' );
+ if ( null !== $data_type->get_first_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) {
return 'auto_increment';
}
@@ -1015,9 +1015,9 @@ private function get_column_extra( WP_Parser_Node $node ): string {
}
}
- if ( $node->get_descendant_token( WP_MySQL_Lexer::VIRTUAL_SYMBOL ) ) {
+ if ( $node->get_first_descendant_token( WP_MySQL_Lexer::VIRTUAL_SYMBOL ) ) {
$extras[] = 'VIRTUAL GENERATED';
- } elseif ( $node->get_descendant_token( WP_MySQL_Lexer::STORED_SYMBOL ) ) {
+ } elseif ( $node->get_first_descendant_token( WP_MySQL_Lexer::STORED_SYMBOL ) ) {
$extras[] = 'STORED GENERATED';
}
return implode( ' ', $extras );
@@ -1026,14 +1026,14 @@ private function get_column_extra( WP_Parser_Node $node ): string {
private function get_column_comment( WP_Parser_Node $node ): string {
foreach ( $node->get_descendant_nodes( 'columnAttribute' ) as $attr ) {
if ( $attr->has_child_token( WP_MySQL_Lexer::COMMENT_SYMBOL ) ) {
- return $this->get_value( $attr->get_child_node( 'textLiteral' ) );
+ return $this->get_value( $attr->get_first_child_node( 'textLiteral' ) );
}
}
return '';
}
private function get_column_data_types( WP_Parser_Node $node ): array {
- $type_node = $node->get_descendant_node( 'dataType' );
+ $type_node = $node->get_first_descendant_node( 'dataType' );
$type = $type_node->get_descendant_tokens();
$token = $type[0];
@@ -1084,7 +1084,7 @@ private function get_column_data_types( WP_Parser_Node $node ): array {
// Get full type.
$full_type = $type;
if ( 'enum' === $type || 'set' === $type ) {
- $string_list = $type_node->get_descendant_node( 'stringList' );
+ $string_list = $type_node->get_first_descendant_node( 'stringList' );
$values = $string_list->get_child_nodes( 'textString' );
foreach ( $values as $i => $value ) {
$values[ $i ] = "'" . str_replace( "'", "''", $this->get_value( $value ) ) . "'";
@@ -1092,7 +1092,7 @@ private function get_column_data_types( WP_Parser_Node $node ): array {
$full_type .= '(' . implode( ',', $values ) . ')';
}
- $field_length = $type_node->get_descendant_node( 'fieldLength' );
+ $field_length = $type_node->get_first_descendant_node( 'fieldLength' );
if ( null !== $field_length ) {
if ( 'decimal' === $type || 'float' === $type || 'double' === $type ) {
$full_type .= rtrim( $this->get_value( $field_length ), ')' ) . ',0)';
@@ -1108,12 +1108,12 @@ private function get_column_data_types( WP_Parser_Node $node ): array {
*/
}
- $precision = $type_node->get_descendant_node( 'precision' );
+ $precision = $type_node->get_first_descendant_node( 'precision' );
if ( null !== $precision ) {
$full_type .= $this->get_value( $precision );
}
- $datetime_precision = $type_node->get_descendant_node( 'typeDatetimePrecision' );
+ $datetime_precision = $type_node->get_first_descendant_node( 'typeDatetimePrecision' );
if ( null !== $datetime_precision ) {
$full_type .= $this->get_value( $datetime_precision );
}
@@ -1136,14 +1136,14 @@ private function get_column_data_types( WP_Parser_Node $node ): array {
// UNSIGNED.
// SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
if (
- $type_node->get_descendant_token( WP_MySQL_Lexer::UNSIGNED_SYMBOL )
- || $type_node->get_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL )
+ $type_node->get_first_descendant_token( WP_MySQL_Lexer::UNSIGNED_SYMBOL )
+ || $type_node->get_first_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL )
) {
$full_type .= ' unsigned';
}
// ZEROFILL.
- if ( $type_node->get_descendant_token( WP_MySQL_Lexer::ZEROFILL_SYMBOL ) ) {
+ if ( $type_node->get_first_descendant_token( WP_MySQL_Lexer::ZEROFILL_SYMBOL ) ) {
$full_type .= ' zerofill';
}
@@ -1169,9 +1169,9 @@ private function get_column_charset_and_collation( WP_Parser_Node $node, string
$is_binary = false;
// Charset.
- $charset_node = $node->get_descendant_node( 'charsetWithOptBinary' );
+ $charset_node = $node->get_first_descendant_node( 'charsetWithOptBinary' );
if ( null !== $charset_node ) {
- $charset_name_node = $charset_node->get_child_node( 'charsetName' );
+ $charset_name_node = $charset_node->get_first_child_node( 'charsetName' );
if ( null !== $charset_name_node ) {
$charset = strtolower( $this->get_value( $charset_name_node ) );
} elseif ( $charset_node->has_child_token( WP_MySQL_Lexer::ASCII_SYMBOL ) ) {
@@ -1189,7 +1189,7 @@ private function get_column_charset_and_collation( WP_Parser_Node $node, string
}
} else {
// National charsets (in MySQL, it's "utf8").
- $data_type_node = $node->get_descendant_node( 'dataType' );
+ $data_type_node = $node->get_first_descendant_node( 'dataType' );
if (
$data_type_node->has_child_node( 'nchar' )
|| $data_type_node->has_child_token( WP_MySQL_Lexer::NCHAR_SYMBOL )
@@ -1206,7 +1206,7 @@ private function get_column_charset_and_collation( WP_Parser_Node $node, string
}
// Collation.
- $collation_node = $node->get_descendant_node( 'collationName' );
+ $collation_node = $node->get_first_descendant_node( 'collationName' );
if ( null !== $collation_node ) {
$collation = strtolower( $this->get_value( $collation_node ) );
}
@@ -1254,7 +1254,7 @@ private function get_column_lengths( WP_Parser_Node $node, string $data_type, ?s
|| 'varchar' === $data_type
|| 'varbinary' === $data_type
) {
- $field_length = $node->get_descendant_node( 'fieldLength' );
+ $field_length = $node->get_first_descendant_node( 'fieldLength' );
if ( null === $field_length ) {
$length = 1;
} else {
@@ -1271,7 +1271,7 @@ private function get_column_lengths( WP_Parser_Node $node, string $data_type, ?s
// For ENUM and SET, we need to check the longest value.
if ( 'enum' === $data_type || 'set' === $data_type ) {
- $string_list = $node->get_descendant_node( 'stringList' );
+ $string_list = $node->get_first_descendant_node( 'stringList' );
$values = $string_list->get_child_nodes( 'textString' );
$length = 0;
foreach ( $values as $value ) {
@@ -1294,13 +1294,13 @@ private function get_column_numeric_attributes( WP_Parser_Node $node, string $da
} elseif ( 'int' === $data_type ) {
return array( 10, 0 );
} elseif ( 'bigint' === $data_type ) {
- if ( null !== $node->get_descendant_token( WP_MySQL_Lexer::UNSIGNED_SYMBOL ) ) {
+ if ( null !== $node->get_first_descendant_token( WP_MySQL_Lexer::UNSIGNED_SYMBOL ) ) {
return array( 20, 0 );
}
// SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
- $data_type = $node->get_descendant_node( 'dataType' );
- if ( null !== $data_type->get_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) {
+ $data_type = $node->get_first_descendant_node( 'dataType' );
+ if ( null !== $data_type->get_first_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) {
return array( 20, 0 );
}
@@ -1309,7 +1309,7 @@ private function get_column_numeric_attributes( WP_Parser_Node $node, string $da
// For bit columns, we need to check the precision.
if ( 'bit' === $data_type ) {
- $field_length = $node->get_descendant_node( 'fieldLength' );
+ $field_length = $node->get_first_descendant_node( 'fieldLength' );
if ( null === $field_length ) {
return array( 1, null );
}
@@ -1319,7 +1319,7 @@ private function get_column_numeric_attributes( WP_Parser_Node $node, string $da
// For floating point numbers, we need to check the precision and scale.
$precision = null;
$scale = null;
- $precision_node = $node->get_descendant_node( 'precision' );
+ $precision_node = $node->get_first_descendant_node( 'precision' );
if ( null !== $precision_node ) {
$values = $precision_node->get_descendant_tokens( WP_MySQL_Lexer::INT_NUMBER );
$precision = (int) $values[0]->value;
@@ -1333,7 +1333,7 @@ private function get_column_numeric_attributes( WP_Parser_Node $node, string $da
} elseif ( 'decimal' === $data_type ) {
if ( null === $precision ) {
// Only precision can be specified ("fieldLength" in the grammar).
- $field_length = $node->get_descendant_node( 'fieldLength' );
+ $field_length = $node->get_first_descendant_node( 'fieldLength' );
if ( null !== $field_length ) {
$precision = (int) trim( $this->get_value( $field_length ), '()' );
}
@@ -1346,7 +1346,7 @@ private function get_column_numeric_attributes( WP_Parser_Node $node, string $da
private function get_column_datetime_precision( WP_Parser_Node $node, string $data_type ): ?int {
if ( 'time' === $data_type || 'datetime' === $data_type || 'timestamp' === $data_type ) {
- $precision = $node->get_descendant_node( 'typeDatetimePrecision' );
+ $precision = $node->get_first_descendant_node( 'typeDatetimePrecision' );
if ( null === $precision ) {
return 0;
} else {
@@ -1357,30 +1357,30 @@ private function get_column_datetime_precision( WP_Parser_Node $node, string $da
}
private function get_column_generation_expression( WP_Parser_Node $node ): string {
- if ( null !== $node->get_descendant_token( WP_MySQL_Lexer::GENERATED_SYMBOL ) ) {
- $expr = $node->get_descendant_node( 'exprWithParentheses' );
+ if ( null !== $node->get_first_descendant_token( WP_MySQL_Lexer::GENERATED_SYMBOL ) ) {
+ $expr = $node->get_first_descendant_node( 'exprWithParentheses' );
return $this->get_value( $expr );
}
return '';
}
private function get_index_name( WP_Parser_Node $node ): string {
- if ( $node->get_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) {
+ if ( $node->get_first_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) {
return 'PRIMARY';
}
- $name_node = $node->get_descendant_node( 'indexName' );
+ $name_node = $node->get_first_descendant_node( 'indexName' );
if ( null === $name_node ) {
/*
* In MySQL, the default index name equals the first column name.
* For functional indexes, the string "functional_index" is used.
* If the name is already used, we need to append a number.
*/
- $subnode = $node->get_child_node( 'keyListVariants' )->get_child_node();
+ $subnode = $node->get_first_child_node( 'keyListVariants' )->get_first_child_node();
if ( 'exprWithParentheses' === $subnode->rule_name ) {
$name = 'functional_index';
} else {
- $name = $this->get_value( $subnode->get_descendant_node( 'identifier' ) );
+ $name = $this->get_value( $subnode->get_first_descendant_node( 'identifier' ) );
}
// @TODO: Check if the name is already used.
@@ -1405,10 +1405,10 @@ private function get_index_type(
bool $has_spatial_column
): string {
// Handle "USING ..." clause.
- $index_type = $node->get_descendant_node( 'indexTypeClause' );
+ $index_type = $node->get_first_descendant_node( 'indexTypeClause' );
if ( null !== $index_type ) {
$index_type = strtoupper(
- $this->get_value( $index_type->get_child_node( 'indexType' ) )
+ $this->get_value( $index_type->get_first_child_node( 'indexType' ) )
);
if ( 'RTREE' === $index_type ) {
return 'SPATIAL';
@@ -1438,7 +1438,7 @@ private function get_index_column_name( WP_Parser_Node $node ): ?string {
if ( 'keyPart' !== $node->rule_name ) {
return null;
}
- return $this->get_value( $node->get_descendant_node( 'identifier' ) );
+ return $this->get_value( $node->get_first_descendant_node( 'identifier' ) );
}
private function get_index_column_collation( WP_Parser_Node $node, string $index_type ): ?string {
@@ -1446,7 +1446,7 @@ private function get_index_column_collation( WP_Parser_Node $node, string $index
return null;
}
- $collate_node = $node->get_descendant_node( 'collationName' );
+ $collate_node = $node->get_first_descendant_node( 'collationName' );
if ( null === $collate_node ) {
return 'A';
}
@@ -1459,7 +1459,7 @@ private function get_index_column_sub_part(
?int $max_length,
bool $is_spatial
): ?int {
- $field_length = $node->get_descendant_node( 'fieldLength' );
+ $field_length = $node->get_first_descendant_node( 'fieldLength' );
if ( null === $field_length ) {
if ( $is_spatial ) {
return 32;
From 8b27c859b47548a0351eb33df582578c8ae073ae Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 5 Feb 2025 14:45:52 +0100
Subject: [PATCH 110/124] Fix FROM/IN handling for SHOW statements
---
tests/WP_SQLite_Driver_Tests.php | 6 +++---
.../sqlite-ast/class-wp-sqlite-driver.php | 20 +++++++++++++++----
2 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index a14bed0..ed4e420 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -691,7 +691,7 @@ public function testShowTableStatusFrom() {
);
$this->assertQuery(
- 'SHOW TABLE STATUS FROM mydb;'
+ 'SHOW TABLE STATUS FROM wp;'
);
$this->assertCount(
@@ -714,7 +714,7 @@ public function testShowTableStatusIn() {
);
$this->assertQuery(
- 'SHOW TABLE STATUS IN mydb;'
+ 'SHOW TABLE STATUS IN wp;'
);
$this->assertCount(
@@ -744,7 +744,7 @@ public function testShowTableStatusInTwoTables() {
);"
);
$this->assertQuery(
- 'SHOW TABLE STATUS IN mydb;'
+ 'SHOW TABLE STATUS IN wp;'
);
$this->assertCount(
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 60cf426..80b55e4 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -1273,8 +1273,14 @@ private function execute_show_index_statement( string $table_name ): void {
private function execute_show_table_status_statement( WP_Parser_Node $node ): void {
// FROM/IN database.
- $in_db = $node->get_first_child_node( 'idDb' );
- $database = null === $in_db ? $this->db_name : $this->translate( $in_db );
+ $in_db = $node->get_first_child_node( 'inDb' );
+ if ( null === $in_db ) {
+ $database = $this->db_name;
+ } else {
+ $database = $this->unquote_sqlite_identifier(
+ $this->translate( $in_db->get_first_child_node( 'identifier' ) )
+ );
+ }
// LIKE and WHERE clauses.
$like_or_where = $node->get_first_child_node( 'likeOrWhere' );
@@ -1325,8 +1331,14 @@ private function execute_show_table_status_statement( WP_Parser_Node $node ): vo
private function execute_show_tables_statement( WP_Parser_Node $node ): void {
// FROM/IN database.
- $in_db = $node->get_first_child_node( 'idDb' );
- $database = null === $in_db ? $this->db_name : $this->translate( $in_db );
+ $in_db = $node->get_first_child_node( 'inDb' );
+ if ( null === $in_db ) {
+ $database = $this->db_name;
+ } else {
+ $database = $this->unquote_sqlite_identifier(
+ $this->translate( $in_db->get_first_child_node( 'identifier' ) )
+ );
+ }
// LIKE and WHERE clauses.
$like_or_where = $node->get_first_child_node( 'likeOrWhere' );
From c27b57102d01baa2af53fbf700f4bb9579bf15cf Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 5 Feb 2025 14:50:16 +0100
Subject: [PATCH 111/124] Add tests for UNION and UNION ALL
---
tests/WP_SQLite_Driver_Tests.php | 55 ++++++++++++++++++++++++++++++++
1 file changed, 55 insertions(+)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index ed4e420..eb3391a 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -3683,4 +3683,59 @@ public function testSelectNonExistentColumn(): void {
'no such column: non_existent_column'
);
}
+
+ public function testUnion(): void {
+ $this->assertQuery( 'CREATE TABLE t (id INT, name VARCHAR(20))' );
+ $this->assertQuery( "INSERT INTO t (id, name) VALUES (1, 'a')" );
+ $this->assertQuery( "INSERT INTO t (id, name) VALUES (2, 'b')" );
+
+ $this->assertQuery(
+ 'SELECT name FROM t WHERE id = 1 UNION SELECT name FROM t WHERE id = 2'
+ );
+ $this->assertEquals(
+ array(
+ (object) array( 'name' => 'a' ),
+ (object) array( 'name' => 'b' ),
+ ),
+ $this->engine->get_query_results()
+ );
+
+ $this->assertQuery(
+ 'SELECT name FROM t WHERE id = 1 UNION SELECT name FROM t WHERE id = 1'
+ );
+ $this->assertEquals(
+ array(
+ (object) array( 'name' => 'a' ),
+ ),
+ $this->engine->get_query_results()
+ );
+ }
+
+ public function testUnionAll(): void {
+ $this->assertQuery( 'CREATE TABLE t (id INT, name VARCHAR(20))' );
+ $this->assertQuery( "INSERT INTO t (id, name) VALUES (1, 'a')" );
+ $this->assertQuery( "INSERT INTO t (id, name) VALUES (2, 'b')" );
+
+ $this->assertQuery(
+ 'SELECT name FROM t WHERE id = 1 UNION SELECT name FROM t WHERE id = 2'
+ );
+ $this->assertEquals(
+ array(
+ (object) array( 'name' => 'a' ),
+ (object) array( 'name' => 'b' ),
+ ),
+ $this->engine->get_query_results()
+ );
+
+ $this->assertQuery(
+ 'SELECT name FROM t WHERE id = 1 UNION ALL SELECT name FROM t WHERE id = 1'
+ );
+ $this->assertEquals(
+ array(
+ (object) array( 'name' => 'a' ),
+ (object) array( 'name' => 'a' ),
+ ),
+ $this->engine->get_query_results()
+ );
+ }
}
From 0f7060fbcbbdcd63ba7784cecb39b04178d5459b Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 5 Feb 2025 16:41:14 +0100
Subject: [PATCH 112/124] Configure the SQLite driver using an $options array
---
tests/WP_SQLite_Driver_Tests.php | 7 +-
tests/WP_SQLite_Driver_Translation_Tests.php | 7 +-
tests/tools/dump-sqlite-query.php | 7 +-
.../sqlite-ast/class-wp-sqlite-driver.php | 111 ++++++++++++------
wp-includes/sqlite/class-wp-sqlite-db.php | 9 +-
5 files changed, 100 insertions(+), 41 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index eb3391a..fc86f54 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -35,7 +35,12 @@ public static function setUpBeforeClass(): void {
public function setUp(): void {
$this->sqlite = new PDO( 'sqlite::memory:' );
- $this->engine = new WP_SQLite_Driver( 'wp', $this->sqlite );
+ $this->engine = new WP_SQLite_Driver(
+ array(
+ 'connection' => $this->sqlite,
+ 'database' => 'wp',
+ )
+ );
$this->engine->query(
"CREATE TABLE _options (
ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 0da6b1a..9b159ec 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -23,7 +23,12 @@ public static function setUpBeforeClass(): void {
}
public function setUp(): void {
- $this->driver = new WP_SQLite_Driver( 'wp', new PDO( 'sqlite::memory:' ) );
+ $this->driver = new WP_SQLite_Driver(
+ array(
+ 'path' => ':memory:',
+ 'database' => 'wp',
+ )
+ );
}
public function testSelect(): void {
diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php
index 8f1aaaf..80977fd 100644
--- a/tests/tools/dump-sqlite-query.php
+++ b/tests/tools/dump-sqlite-query.php
@@ -11,7 +11,12 @@
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
-$driver = new WP_SQLite_Driver( 'wp', new PDO( 'sqlite::memory:' ) );
+$driver = new WP_SQLite_Driver(
+ array(
+ 'path' => ':memory:',
+ 'database' => 'wp',
+ )
+);
$query = "SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id WHERE t1.name = 'abc'";
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 80b55e4..3257b1a 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -311,68 +311,105 @@ class WP_SQLite_Driver {
/**
* Constructor.
*
- * Create PDO object, set user defined functions and initialize other settings.
- * Don't use parent::__construct() because this class does not only returns
- * PDO instance but many others jobs.
+ * Set up an SQLite connection and the MySQL-on-SQLite driver.
*
- * @param string $db_name The database name. In WordPress, the value of DB_NAME.
- * @param PDO|null $pdo The PDO object.
+ * @param array $options {
+ * An array of options.
+ *
+ * @type string $database Database name.
+ * The name of the emulated MySQL database.
+ * @type string|null $path Optional. SQLite database path.
+ * For in-memory database, use ':memory:'.
+ * Must be set when PDO instance is not provided.
+ * @type PDO|null $connection Optional. PDO instance with SQLite connection.
+ * If not provided, a new PDO instance will be created.
+ * @type int|null $timeout Optional. SQLite timeout in seconds.
+ * The time to wait for a writable lock.
+ * @type string|null $sqlite_journal_mode Optional. SQLite journal mode.
+ * }
*/
- public function __construct( string $db_name, ?PDO $pdo = null ) {
- $this->db_name = $db_name;
- if ( ! $pdo ) {
- if ( ! is_file( FQDB ) ) {
+ public function __construct( array $options ) {
+ // Database name.
+ if ( ! isset( $options['database'] ) || ! is_string( $options['database'] ) ) {
+ throw new InvalidArgumentException( 'Option "database" is required.' );
+ }
+ $this->db_name = $options['database'];
+
+ // Database connection.
+ if ( isset( $options['connection'] ) && $options['connection'] instanceof PDO ) {
+ $this->pdo = $options['connection'];
+ }
+
+ // Create a PDO connection if it is not provided.
+ if ( ! $this->pdo ) {
+ if ( ! isset( $options['path'] ) || ! is_string( $options['path'] ) ) {
+ throw new InvalidArgumentException( 'Option "path" is required when "connection" is not provided.' );
+ }
+ $path = $options['path'];
+
+ if ( ':memory:' !== $path && ! is_file( $path ) ) {
$this->prepare_directory();
}
try {
- $options = array(
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
- PDO::ATTR_STRINGIFY_FETCHES => true,
- PDO::ATTR_TIMEOUT => self::DEFAULT_TIMEOUT,
- );
-
- $dsn = 'sqlite:' . FQDB;
- $pdo = new PDO( $dsn, null, null, $options );
- } catch ( PDOException $ex ) {
+ $this->pdo = new PDO( 'sqlite:' . $path );
+ } catch ( PDOException $e ) {
$this->error_messages[] = sprintf(
'%s
%s
%s
',
'Database initialization error!',
- 'Code: ' . $ex->getCode(),
- 'Error Message: ' . $ex->getMessage()
+ 'Code: ' . $e->getCode(),
+ 'Error Message: ' . $e->getMessage()
);
return;
}
}
- WP_SQLite_PDO_User_Defined_Functions::register_for( $pdo );
+ // Throw exceptions on error.
+ $this->pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
- // MySQL data comes across stringified by default.
- $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
+ // Configure SQLite timeout.
+ if ( isset( $options['timeout'] ) && is_int( $options['timeout'] ) ) {
+ $timeout = $options['timeout'];
+ } else {
+ $timeout = self::DEFAULT_TIMEOUT;
+ }
+ $this->pdo->setAttribute( PDO::ATTR_TIMEOUT, $timeout );
- $this->pdo = $pdo;
+ // Return all values (except null) as strings.
+ $this->pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
- $this->information_schema_builder = new WP_SQLite_Information_Schema_Builder(
- $this->db_name,
- array( $this, 'execute_sqlite_query' )
- );
- $this->information_schema_builder->ensure_information_schema_tables();
+ // Enable foreign keys. By default, they are off.
+ $this->pdo->query( 'PRAGMA foreign_keys = ON' );
+
+ // Configure SQLite journal mode.
+ if (
+ isset( $options['sqlite_journal_mode'] )
+ && in_array(
+ $options['sqlite_journal_mode'],
+ array( 'DELETE', 'TRUNCATE', 'PERSIST', 'MEMORY', 'WAL', 'OFF' ),
+ true
+ )
+ ) {
+ $this->pdo->query( 'PRAGMA journal_mode = ' . $options['sqlite_journal_mode'] );
+ }
+
+ // Register SQLite functions.
+ WP_SQLite_PDO_User_Defined_Functions::register_for( $this->pdo );
// Load MySQL grammar.
if ( null === self::$grammar ) {
self::$grammar = new WP_Parser_Grammar( require self::GRAMMAR_PATH );
}
+ // Initialize information schema builder.
+ $this->information_schema_builder = new WP_SQLite_Information_Schema_Builder(
+ $this->db_name,
+ array( $this, 'execute_sqlite_query' )
+ );
+ $this->information_schema_builder->ensure_information_schema_tables();
+
// Load SQLite version to a property used by WordPress health info.
$this->client_info = $this->get_sqlite_version();
-
- $this->pdo->query( 'PRAGMA foreign_keys = ON' );
- $this->pdo->query( 'PRAGMA encoding="UTF-8";' );
-
- $valid_journal_modes = array( 'DELETE', 'TRUNCATE', 'PERSIST', 'MEMORY', 'WAL', 'OFF' );
- if ( defined( 'SQLITE_JOURNAL_MODE' ) && in_array( SQLITE_JOURNAL_MODE, $valid_journal_modes, true ) ) {
- $this->pdo->query( 'PRAGMA journal_mode = ' . SQLITE_JOURNAL_MODE );
- }
}
/**
diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php
index cdb214d..84ea3ea 100644
--- a/wp-includes/sqlite/class-wp-sqlite-db.php
+++ b/wp-includes/sqlite/class-wp-sqlite-db.php
@@ -239,7 +239,14 @@ public function db_connect( $allow_bail = true ) {
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php';
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
- $this->dbh = new WP_SQLite_Driver( 'wp', $pdo );
+ $this->dbh = new WP_SQLite_Driver(
+ array(
+ 'connection' => $pdo,
+ 'path' => FQDB,
+ 'database' => $this->dbname,
+ 'sqlite_journal_mode' => defined( 'SQLITE_JOURNAL_MODE' ) ? SQLITE_JOURNAL_MODE : null,
+ )
+ );
} else {
$this->dbh = new WP_SQLite_Translator( $pdo );
}
From e961bf50cd156b2caea291bb019b106c7b4c47ac Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 6 Feb 2025 11:26:18 +0100
Subject: [PATCH 113/124] Use custom exception class for driver errors
---
tests/WP_SQLite_Driver_Tests.php | 1 +
tests/WP_SQLite_Driver_Translation_Tests.php | 1 +
tests/tools/dump-sqlite-query.php | 3 +-
.../class-wp-sqlite-driver-exception.php | 3 +
.../sqlite-ast/class-wp-sqlite-driver.php | 70 +++++++++++--------
wp-includes/sqlite/class-wp-sqlite-db.php | 1 +
6 files changed, 48 insertions(+), 31 deletions(-)
create mode 100644 wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index fc86f54..4805924 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -2,6 +2,7 @@
require_once __DIR__ . '/WP_SQLite_Translator_Tests.php';
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
+require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php';
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
use PHPUnit\Framework\TestCase;
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 9b159ec..7b427cd 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -1,6 +1,7 @@
db_name = $options['database'];
@@ -343,7 +343,9 @@ public function __construct( array $options ) {
// Create a PDO connection if it is not provided.
if ( ! $this->pdo ) {
if ( ! isset( $options['path'] ) || ! is_string( $options['path'] ) ) {
- throw new InvalidArgumentException( 'Option "path" is required when "connection" is not provided.' );
+ throw new WP_SQLite_Driver_Exception(
+ 'Option "path" is required when "connection" is not provided.'
+ );
}
$path = $options['path'];
@@ -486,7 +488,7 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
$ast = $parser->parse();
if ( null === $ast ) {
- throw new Exception( 'Failed to parse the MySQL query.' );
+ throw new WP_SQLite_Driver_Exception( 'Failed to parse the MySQL query.' );
}
// Handle transaction commands.
@@ -580,17 +582,7 @@ public function execute_sqlite_query( $sql, $params = array() ) {
);
$stmt = $this->pdo->prepare( $sql );
- if ( false === $stmt || null === $stmt ) {
- $info = $this->pdo->errorInfo();
- throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] );
- }
-
- $is_success = $stmt->execute( $params );
- if ( false === $is_success ) {
- $info = $stmt->errorInfo();
- throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] );
- }
-
+ $stmt->execute( $params );
return $stmt;
}
@@ -694,12 +686,16 @@ public function rollback(): void {
*/
private function execute_mysql_query( WP_Parser_Node $ast ) {
if ( 'query' !== $ast->rule_name ) {
- throw new Exception( sprintf( 'Expected "query" node, got: "%s"', $ast->rule_name ) );
+ throw new WP_SQLite_Driver_Exception(
+ sprintf( 'Expected "query" node, got: "%s"', $ast->rule_name )
+ );
}
$children = $ast->get_child_nodes();
if ( count( $children ) !== 1 ) {
- throw new Exception( sprintf( 'Expected 1 child, got: %d', count( $children ) ) );
+ throw new WP_SQLite_Driver_Exception(
+ sprintf( 'Expected 1 child, got: %d', count( $children ) )
+ );
}
$ast = $children[0]->get_first_child_node();
@@ -724,9 +720,9 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$this->execute_create_table_statement( $ast );
break;
default:
- throw new Exception(
+ throw $this->not_supported_exception(
sprintf(
- 'Unsupported statement type: "%s" > "%s"',
+ 'statement type: "%s" > "%s"',
$ast->rule_name,
$subtree->rule_name
)
@@ -740,9 +736,9 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$this->execute_alter_table_statement( $ast );
break;
default:
- throw new Exception(
+ throw $this->not_supported_exception(
sprintf(
- 'Unsupported statement type: "%s" > "%s"',
+ 'statement type: "%s" > "%s"',
$ast->rule_name,
$subtree->rule_name
)
@@ -778,9 +774,9 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$this->execute_describe_statement( $subtree );
break;
default:
- throw new Exception(
+ throw $this->not_supported_exception(
sprintf(
- 'Unsupported statement type: "%s" > "%s"',
+ 'statement type: "%s" > "%s"',
$ast->rule_name,
$subtree->rule_name
)
@@ -788,7 +784,9 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
}
break;
default:
- throw new Exception( sprintf( 'Unsupported statement type: "%s"', $ast->rule_name ) );
+ throw $this->not_supported_exception(
+ sprintf( 'statement type: "%s"', $ast->rule_name )
+ );
}
}
@@ -1265,9 +1263,9 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
$this->results = true;
return;
default:
- throw new Exception(
+ throw $this->not_supported_exception(
sprintf(
- 'Unsupported statement type: "%s" > "%s"',
+ 'statement type: "%s" > "%s"',
$node->rule_name,
$keyword1->value
)
@@ -1449,7 +1447,12 @@ private function translate( $ast ) {
}
if ( ! $ast instanceof WP_Parser_Node ) {
- throw new Exception( 'translate_query only accepts WP_MySQL_Token and WP_Parser_Node instances' );
+ throw new WP_SQLite_Driver_Exception(
+ sprintf(
+ 'Expected a WP_Parser_Node or WP_MySQL_Token instance, got: %s',
+ gettype( $ast )
+ )
+ );
}
$rule_name = $ast->rule_name;
@@ -1843,7 +1846,12 @@ private function translate_function_call( WP_Parser_Node $node ): string {
$format = strtr( $mysql_format, self::DATE_FORMAT_TO_STRFTIME_MAP );
if ( ! $format ) {
- throw new Exception( "Could not translate a DATE_FORMAT() format to STRFTIME format ($mysql_format)" );
+ throw new WP_SQLite_Driver_Exception(
+ sprintf(
+ 'Could not translate a DATE_FORMAT() format to STRFTIME format (%s)',
+ $mysql_format
+ )
+ );
}
/*
@@ -1967,7 +1975,9 @@ private function get_sqlite_create_table_statement( string $table_name, ?string
)->fetch( PDO::FETCH_ASSOC );
if ( false === $table_info ) {
- throw new Exception( 'Table not found in information_schema' );
+ throw new WP_SQLite_Driver_Exception(
+ sprintf( 'Table "%s" not found in information schema', $table_name )
+ );
}
// 2. Get column info.
@@ -2424,11 +2434,11 @@ private function set_error( $line, $function_name, $message ) {
}
private function invalid_input_exception() {
- throw new Exception( 'MySQL query syntax error.' );
+ throw new WP_SQLite_Driver_Exception( 'MySQL query syntax error.' );
}
private function not_supported_exception( string $cause ): Exception {
- return new Exception(
+ return new WP_SQLite_Driver_Exception(
sprintf( 'MySQL query not supported. Cause: %s', $cause )
);
}
diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php
index 84ea3ea..68b7e86 100644
--- a/wp-includes/sqlite/class-wp-sqlite-db.php
+++ b/wp-includes/sqlite/class-wp-sqlite-db.php
@@ -238,6 +238,7 @@ public function db_connect( $allow_bail = true ) {
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php';
require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php';
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
+ require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php';
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
$this->dbh = new WP_SQLite_Driver(
array(
From fda096c08be537a64815f9f67c3fea08c6378524 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 6 Feb 2025 12:30:22 +0100
Subject: [PATCH 114/124] Ignore transaction rollback errors when an exception
occurs
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 94ac09b..c50d134 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -535,12 +535,16 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
$this->execute_mysql_query( $ast );
$this->commit();
return $this->return_value;
- } catch ( Exception $err ) {
- $this->rollback();
+ } catch ( Throwable $e ) {
+ try {
+ $this->rollback();
+ } catch ( Throwable $rollback_exception ) {
+ // Ignore rollback errors.
+ }
if ( defined( 'PDO_DEBUG' ) && PDO_DEBUG === true ) {
- throw $err;
+ throw $e;
}
- return $this->handle_error( $err );
+ return $this->handle_error( $e );
}
}
From e4df6b9b5726d4443f109e277d1686797a5aa50b Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 6 Feb 2025 12:30:41 +0100
Subject: [PATCH 115/124] Return true for transactional commands
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index c50d134..7175b32 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -494,7 +494,8 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
// Handle transaction commands.
$child = $ast->get_first_child();
if ( $child instanceof WP_Parser_Node && 'beginWork' === $child->rule_name ) {
- return $this->begin_transaction();
+ $this->begin_transaction();
+ return true;
}
if ( $child instanceof WP_Parser_Node && 'simpleStatement' === $child->rule_name ) {
@@ -507,25 +508,29 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
WP_MySQL_Lexer::START_SYMBOL === $token1->id
&& WP_MySQL_Lexer::TRANSACTION_SYMBOL === $token2->id
) {
- return $this->begin_transaction();
+ $this->begin_transaction();
+ return true;
}
if (
WP_MySQL_Lexer::BEGIN_SYMBOL === $token1->id
) {
- return $this->begin_transaction();
+ $this->begin_transaction();
+ return true;
}
if (
WP_MySQL_Lexer::COMMIT_SYMBOL === $token1->id
) {
- return $this->commit();
+ $this->commit();
+ return true;
}
if (
WP_MySQL_Lexer::ROLLBACK_SYMBOL === $token1->id
) {
- return $this->rollback();
+ $this->rollback();
+ return true;
}
}
}
From 96e020857c8dfa6b2d4ee0fdffd461169573bca8 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 6 Feb 2025 13:35:55 +0100
Subject: [PATCH 116/124] Move database directory creation outside of the
driver, revamp it
---
.../sqlite-ast/class-wp-sqlite-driver.php | 49 -------------------
wp-includes/sqlite/class-wp-sqlite-db.php | 48 ++++++++++++++++++
2 files changed, 48 insertions(+), 49 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 7175b32..effef51 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -349,10 +349,6 @@ public function __construct( array $options ) {
}
$path = $options['path'];
- if ( ':memory:' !== $path && ! is_file( $path ) ) {
- $this->prepare_directory();
- }
-
try {
$this->pdo = new PDO( 'sqlite:' . $path );
} catch ( PDOException $e ) {
@@ -2321,51 +2317,6 @@ private function quote_mysql_identifier( string $unquoted_identifier ): string {
return '`' . str_replace( '`', '``', $unquoted_identifier ) . '`';
}
- /**
- * This method makes database directory and .htaccess file.
- *
- * It is executed only once when the installation begins.
- */
- private function prepare_directory() {
- $u = umask( 0000 );
- if ( ! is_dir( FQDBDIR ) ) {
- if ( ! @mkdir( FQDBDIR, 0704, true ) ) {
- umask( $u );
- wp_die( 'Unable to create the required directory! Please check your server settings.', 'Error!' );
- }
- }
- if ( ! is_writable( FQDBDIR ) ) {
- umask( $u );
- $message = 'Unable to create a file in the directory! Please check your server settings.';
- wp_die( $message, 'Error!' );
- }
- if ( ! is_file( FQDBDIR . '.htaccess' ) ) {
- $fh = fopen( FQDBDIR . '.htaccess', 'w' );
- if ( ! $fh ) {
- umask( $u );
- echo 'Unable to create a file in the directory! Please check your server settings.';
-
- return false;
- }
- fwrite( $fh, 'DENY FROM ALL' );
- fclose( $fh );
- }
- if ( ! is_file( FQDBDIR . 'index.php' ) ) {
- $fh = fopen( FQDBDIR . 'index.php', 'w' );
- if ( ! $fh ) {
- umask( $u );
- echo 'Unable to create a file in the directory! Please check your server settings.';
-
- return false;
- }
- fwrite( $fh, '' );
- fclose( $fh );
- }
- umask( $u );
-
- return true;
- }
-
/**
* Method to clear previous data.
*/
diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php
index 68b7e86..e634079 100644
--- a/wp-includes/sqlite/class-wp-sqlite-db.php
+++ b/wp-includes/sqlite/class-wp-sqlite-db.php
@@ -240,6 +240,7 @@ public function db_connect( $allow_bail = true ) {
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php';
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
+ $this->ensure_database_directory( FQDB );
$this->dbh = new WP_SQLite_Driver(
array(
'connection' => $pdo,
@@ -442,4 +443,51 @@ public function db_version() {
public function db_server_info() {
return $this->dbh->get_sqlite_version();
}
+
+ /**
+ * Make sure the SQLite database directory exists and is writable.
+ * Create .htaccess and index.php files to prevent direct access.
+ *
+ * @param string $database_path The path to the SQLite database file.
+ */
+ private function ensure_database_directory( string $database_path ) {
+ $dir = dirname( $database_path );
+
+ // Set the umask to 0000 to apply permissions exactly as specified.
+ // A non-zero umask affects new file and directory permissions.
+ $umask = umask( 0 );
+
+ // Ensure database directory.
+ if ( ! is_dir( $dir ) ) {
+ if ( ! @mkdir( $dir, 0700, true ) ) {
+ wp_die( sprintf( 'Failed to create database directory: %s', $dir ), 'Error!' );
+ }
+ }
+ if ( ! is_writable( $dir ) ) {
+ wp_die( sprintf( 'Database directory is not writable: %s', $dir ), 'Error!' );
+ }
+
+ // Ensure .htaccess file to prevent direct access.
+ $path = $dir . DIRECTORY_SEPARATOR . '.htaccess';
+ if ( ! is_file( $path ) ) {
+ $result = file_put_contents( $path, 'DENY FROM ALL', LOCK_EX );
+ if ( false === $result ) {
+ wp_die( sprintf( 'Failed to create file: %s', $path ), 'Error!' );
+ }
+ chmod( $path, 0600 );
+ }
+
+ // Ensure index.php file to prevent direct access.
+ $path = $dir . DIRECTORY_SEPARATOR . 'index.php';
+ if ( ! is_file( $path ) ) {
+ $result = file_put_contents( $path, '', LOCK_EX );
+ if ( false === $result ) {
+ wp_die( sprintf( 'Failed to create file: %s', $path ), 'Error!' );
+ }
+ chmod( $path, 0600 );
+ }
+
+ // Restore the original umask value.
+ umask( $umask );
+ }
}
From cb4d6a3b32239ce9a9e11d1759f0fff419452ad8 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 6 Feb 2025 13:42:57 +0100
Subject: [PATCH 117/124] Extract debug mode setting to an option
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 13 ++++++++++++-
wp-includes/sqlite/class-wp-sqlite-db.php | 1 +
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index effef51..2d14aa7 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -232,6 +232,13 @@ class WP_SQLite_Driver {
*/
private $pdo;
+ /**
+ * Enables debug mode.
+ *
+ * @var bool
+ */
+ private $debug;
+
/**
* Last executed MySQL query.
*
@@ -323,6 +330,7 @@ class WP_SQLite_Driver {
* Must be set when PDO instance is not provided.
* @type PDO|null $connection Optional. PDO instance with SQLite connection.
* If not provided, a new PDO instance will be created.
+ * @type bool $debug Optional. Enable debug mode.
* @type int|null $timeout Optional. SQLite timeout in seconds.
* The time to wait for a writable lock.
* @type string|null $sqlite_journal_mode Optional. SQLite journal mode.
@@ -340,6 +348,9 @@ public function __construct( array $options ) {
$this->pdo = $options['connection'];
}
+ // Debug mode.
+ $this->debug = isset( $options['debug'] ) && true === $options['debug'];
+
// Create a PDO connection if it is not provided.
if ( ! $this->pdo ) {
if ( ! isset( $options['path'] ) || ! is_string( $options['path'] ) ) {
@@ -542,7 +553,7 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
} catch ( Throwable $rollback_exception ) {
// Ignore rollback errors.
}
- if ( defined( 'PDO_DEBUG' ) && PDO_DEBUG === true ) {
+ if ( true === $this->debug ) {
throw $e;
}
return $this->handle_error( $e );
diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php
index e634079..decc486 100644
--- a/wp-includes/sqlite/class-wp-sqlite-db.php
+++ b/wp-includes/sqlite/class-wp-sqlite-db.php
@@ -244,6 +244,7 @@ public function db_connect( $allow_bail = true ) {
$this->dbh = new WP_SQLite_Driver(
array(
'connection' => $pdo,
+ 'debug' => defined( 'PDO_DEBUG' ) && true === PDO_DEBUG,
'path' => FQDB,
'database' => $this->dbname,
'sqlite_journal_mode' => defined( 'SQLITE_JOURNAL_MODE' ) ? SQLITE_JOURNAL_MODE : null,
From 54c27a2f557a301a9ecae754877202dd963f0c9e Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 6 Feb 2025 15:45:19 +0100
Subject: [PATCH 118/124] Improve driver docs and property naming
---
tests/WP_SQLite_Driver_Translation_Tests.php | 4 +-
tests/tools/dump-sqlite-query.php | 2 +-
.../sqlite-ast/class-wp-sqlite-driver.php | 160 +++++++++---------
wp-includes/sqlite/class-wp-sqlite-db.php | 2 +-
4 files changed, 86 insertions(+), 82 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 7b427cd..0517548 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -1261,7 +1261,7 @@ private function assertQuery( $expected, string $query ): void {
);
}
- $executed_queries = array_column( $this->driver->get_sqlite_queries(), 'sql' );
+ $executed_queries = array_column( $this->driver->get_last_sqlite_queries(), 'sql' );
// Remove BEGIN and COMMIT/ROLLBACK queries.
if ( count( $executed_queries ) > 2 ) {
@@ -1305,7 +1305,7 @@ function ( $query ) {
private function assertExecutedInformationSchemaQueries( array $expected ): void {
// Collect and normalize "information_schema" queries.
$queries = array();
- foreach ( $this->driver->get_sqlite_queries() as $query ) {
+ foreach ( $this->driver->get_last_sqlite_queries() as $query ) {
if ( ! str_contains( $query['sql'], '_mysql_information_schema_' ) ) {
continue;
}
diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php
index 96d048c..3282262 100644
--- a/tests/tools/dump-sqlite-query.php
+++ b/tests/tools/dump-sqlite-query.php
@@ -23,7 +23,7 @@
$driver->query( $query );
-$executed_queries = $driver->get_sqlite_queries();
+$executed_queries = $driver->get_last_sqlite_queries();
if ( count( $executed_queries ) > 2 ) {
// Remove BEGIN and COMMIT/ROLLBACK queries.
$executed_queries = array_values( array_slice( $executed_queries, 1, -1, true ) );
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 2d14aa7..977d1cf 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -5,7 +5,19 @@
* phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO
*/
+/**
+ * SQLite driver for MySQL.
+ *
+ * This class emulates a MySQL database server on top of an SQLite database.
+ * It translates queries written in MySQL SQL dialect to an SQLite SQL dialect,
+ * maintains necessary metadata, and executes the translated queries in SQLite.
+ *
+ * The driver requires PDO with the SQLite driver, and the PCRE engine.
+ */
class WP_SQLite_Driver {
+ /**
+ * The path to the MySQL SQL grammar file.
+ */
const GRAMMAR_PATH = __DIR__ . '/../../wp-includes/mysql/mysql-grammar.php';
/**
@@ -14,7 +26,7 @@ class WP_SQLite_Driver {
const DEFAULT_TIMEOUT = 10;
/**
- * An identifier prefix for internal objects.
+ * An identifier prefix for internal database objects.
*
* @TODO: Do not allow accessing objects with this prefix.
*/
@@ -151,16 +163,16 @@ class WP_SQLite_Driver {
);
/**
- * The MySQL to SQLite date formats translation.
+ * A map of MySQL to SQLite date format translation.
*
- * Maps MySQL formats to SQLite strftime() formats.
+ * It maps MySQL DATE_FORMAT() formats to SQLite STRFTIME() formats.
*
* For MySQL formats, see:
- * * https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_date-format
+ * https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_date-format
*
* For SQLite formats, see:
- * * https://www.sqlite.org/lang_datefunc.html
- * * https://strftime.org/
+ * https://www.sqlite.org/lang_datefunc.html
+ * https://strftime.org/
*/
const DATE_FORMAT_TO_STRFTIME_MAP = array(
'%a' => '%D',
@@ -196,7 +208,7 @@ class WP_SQLite_Driver {
);
/**
- * The SQLite version.
+ * The SQLite engine version.
*
* This is a mysqli-like property that is needed to avoid a PHP warning in
* the WordPress health info. The "WP_Debug_Data::get_wp_database()" method
@@ -212,109 +224,101 @@ class WP_SQLite_Driver {
public $client_info;
/**
+ * A MySQL query parser grammar.
+ *
* @var WP_Parser_Grammar
*/
- private static $grammar;
+ private static $mysql_grammar;
/**
- * The database name. In WordPress, the value of DB_NAME.
+ * The database name.
*
* @var string
*/
private $db_name;
/**
- * Class variable to reference to the PDO instance.
- *
- * @access private
+ * An instance of the PDO object.
*
- * @var PDO object
+ * @var PDO
*/
private $pdo;
/**
- * Enables debug mode.
+ * Whether a debug mode is enabled.
*
* @var bool
*/
private $debug;
+ /**
+ * @var WP_SQLite_Information_Schema_Builder
+ */
+ private $information_schema_builder;
+
/**
* Last executed MySQL query.
*
* @var string
*/
- private $mysql_query;
+ private $last_mysql_query;
/**
- * A list of executed SQLite queries.
+ * A list of SQLite queries executed for the last MySQL query.
*
* @var array
*/
- private $sqlite_queries = array();
+ private $last_sqlite_queries = array();
/**
- * Class variable to store the result of the query.
- *
- * @access private
+ * Results of the last emulated query.
*
- * @var array reference to the PHP object
+ * @var array|null
*/
- private $results = null;
+ private $last_result;
/**
- * Class variable to store the file name and function to cause error.
+ * Return value of the last emulated query.
*
- * @access private
- *
- * @var array
+ * @var mixed
*/
- private $errors;
+ private $last_return_value;
/**
- * Class variable to store the error messages.
- *
- * @access private
+ * Number of rows found by the last SQL_CALC_FOUND_ROW query.
*
- * @var array
+ * @var int
*/
- private $error_messages = array();
+ private $last_sql_calc_found_rows = null;
/**
- * Number of rows found by the last SQL_CALC_FOUND_ROW query.
+ * Error encountered during the last query.
*
- * @var int integer
+ * @var array
*/
- private $last_sql_calc_found_rows = null;
+ private $errors;
/**
- * Return value from query().
- *
- * Each query has its own return value.
+ * Error messages produced by the last query.
*
- * @var mixed
+ * @var array
*/
- private $return_value;
+ private $error_messages = array();
/**
- * Variable to keep track of nested transactions level.
+ * Transaction nesting level of the executed SQLite queries.
*
* @var int
*/
private $transaction_level = 0;
/**
- * The PDO fetch mode passed to query().
+ * The PDO fetch mode used for the emulated query.
*
* @var mixed
*/
private $pdo_fetch_mode;
- /**
- * @var WP_SQLite_Information_Schema_Builder
- */
- private $information_schema_builder;
-
/**
* Constructor.
*
@@ -406,8 +410,8 @@ public function __construct( array $options ) {
WP_SQLite_PDO_User_Defined_Functions::register_for( $this->pdo );
// Load MySQL grammar.
- if ( null === self::$grammar ) {
- self::$grammar = new WP_Parser_Grammar( require self::GRAMMAR_PATH );
+ if ( null === self::$mysql_grammar ) {
+ self::$mysql_grammar = new WP_Parser_Grammar( require self::GRAMMAR_PATH );
}
// Initialize information schema builder.
@@ -444,8 +448,8 @@ public function get_sqlite_version(): string {
*
* @return string|null
*/
- public function get_mysql_query(): ?string {
- return $this->mysql_query;
+ public function get_last_mysql_query(): ?string {
+ return $this->last_mysql_query;
}
/**
@@ -453,8 +457,8 @@ public function get_mysql_query(): ?string {
*
* @return array
*/
- public function get_sqlite_queries(): array {
- return $this->sqlite_queries;
+ public function get_last_sqlite_queries(): array {
+ return $this->last_sqlite_queries;
}
/**
@@ -483,15 +487,15 @@ public function get_insert_id() {
*/
public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) {
$this->flush();
- $this->pdo_fetch_mode = $fetch_mode;
- $this->mysql_query = $query;
+ $this->pdo_fetch_mode = $fetch_mode;
+ $this->last_mysql_query = $query;
try {
// Parse the MySQL query.
$lexer = new WP_MySQL_Lexer( $query );
$tokens = $lexer->remaining_tokens();
- $parser = new WP_MySQL_Parser( self::$grammar, $tokens );
+ $parser = new WP_MySQL_Parser( self::$mysql_grammar, $tokens );
$ast = $parser->parse();
if ( null === $ast ) {
@@ -546,7 +550,7 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
$this->begin_transaction();
$this->execute_mysql_query( $ast );
$this->commit();
- return $this->return_value;
+ return $this->last_return_value;
} catch ( Throwable $e ) {
try {
$this->rollback();
@@ -566,7 +570,7 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
* @return mixed
*/
public function get_query_results() {
- return $this->results;
+ return $this->last_result;
}
/**
@@ -574,8 +578,8 @@ public function get_query_results() {
*
* @return mixed
*/
- public function get_return_value() {
- return $this->return_value;
+ public function get_last_return_value() {
+ return $this->last_return_value;
}
/**
@@ -592,7 +596,7 @@ public function get_return_value() {
* }
*/
public function execute_sqlite_query( $sql, $params = array() ) {
- $this->sqlite_queries[] = array(
+ $this->last_sqlite_queries[] = array(
'sql' => $sql,
'params' => $params,
);
@@ -618,10 +622,10 @@ public function get_error_message() {
$output = '
' . PHP_EOL;
$output .= '' . PHP_EOL;
$output .= '
MySQL query:
' . PHP_EOL;
- $output .= '
' . $this->mysql_query . '
' . PHP_EOL;
+ $output .= '
' . $this->last_mysql_query . '
' . PHP_EOL;
$output .= '
Queries made or created this session were:
' . PHP_EOL;
$output .= '
' . PHP_EOL;
- foreach ( $this->sqlite_queries as $q ) {
+ foreach ( $this->last_sqlite_queries as $q ) {
$message = "Executing: {$q['sql']} | " . ( $q['params'] ? 'parameters: ' . implode( ', ', $q['params'] ) : '(no parameters)' );
$output .= '- ' . htmlspecialchars( $message ) . '
' . PHP_EOL;
@@ -778,7 +782,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
* It would be lovely to support at least SET autocommit,
* but I don't think that is even possible with SQLite.
*/
- $this->results = 0;
+ $this->last_result = 0;
break;
case 'showStatement':
$this->execute_show_statement( $ast );
@@ -1007,7 +1011,7 @@ private function execute_delete_statement( WP_Parser_Node $node ): void {
)
);
$this->set_result_from_affected_rows();
- $rows += $this->results;
+ $rows += $this->last_result;
}
}
@@ -1276,7 +1280,7 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
$this->execute_show_tables_statement( $node );
break;
case WP_MySQL_Lexer::VARIABLES_SYMBOL:
- $this->results = true;
+ $this->last_result = true;
return;
default:
throw $this->not_supported_exception(
@@ -1909,8 +1913,8 @@ private function translate_function_call( WP_Parser_Node $node ): string {
// For compatibility with more complex use cases, it may
// be better to register it as a custom SQLite function.
$found_rows = $this->last_sql_calc_found_rows;
- if ( null === $found_rows && is_array( $this->results ) ) {
- $found_rows = count( $this->results );
+ if ( null === $found_rows && is_array( $this->last_result ) ) {
+ $found_rows = count( $this->last_result );
}
return sprintf( "(SELECT %d) AS 'FOUND_ROWS()'", $found_rows );
default:
@@ -2332,11 +2336,11 @@ private function quote_mysql_identifier( string $unquoted_identifier ): string {
* Method to clear previous data.
*/
private function flush() {
- $this->mysql_query = '';
- $this->sqlite_queries = array();
- $this->results = null;
- $this->return_value = null;
- $this->error_messages = array();
+ $this->last_mysql_query = '';
+ $this->last_sqlite_queries = array();
+ $this->last_result = null;
+ $this->last_return_value = null;
+ $this->error_messages = array();
}
/**
@@ -2345,8 +2349,8 @@ private function flush() {
* @param array $data The data to set.
*/
private function set_results_from_fetched_data( $data ) {
- $this->results = $data;
- $this->return_value = $this->results;
+ $this->last_result = $data;
+ $this->last_return_value = $this->last_result;
}
/**
@@ -2366,8 +2370,8 @@ private function set_result_from_affected_rows( $override = null ) {
} else {
$affected_rows = $override;
}
- $this->results = $affected_rows;
- $this->return_value = $affected_rows;
+ $this->last_result = $affected_rows;
+ $this->last_return_value = $affected_rows;
}
/**
@@ -2380,7 +2384,7 @@ private function set_result_from_affected_rows( $override = null ) {
private function handle_error( Exception $err ) {
$message = $err->getMessage();
$this->set_error( __LINE__, __FUNCTION__, $message );
- $this->return_value = false;
+ $this->last_return_value = false;
return false;
}
diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php
index decc486..401d386 100644
--- a/wp-includes/sqlite/class-wp-sqlite-db.php
+++ b/wp-includes/sqlite/class-wp-sqlite-db.php
@@ -332,7 +332,7 @@ public function query( $query ) {
$return_val = true;
} elseif ( preg_match( '/^\s*(insert|delete|update|replace)\s/i', $query ) ) {
if ( $this->dbh instanceof WP_SQLite_Driver ) {
- $this->rows_affected = $this->dbh->get_return_value();
+ $this->rows_affected = $this->dbh->get_last_return_value();
} else {
$this->rows_affected = $this->dbh->get_rows_affected();
}
From beaa51dba2e155bbd6ae8347101c4c576e30425f Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Thu, 6 Feb 2025 16:44:53 +0100
Subject: [PATCH 119/124] Revamp error handling, extract error notice creation
out from the driver
---
tests/WP_SQLite_Driver_Tests.php | 256 ++++++++++--------
tests/WP_SQLite_Driver_Translation_Tests.php | 8 +-
.../class-wp-sqlite-driver-exception.php | 23 +-
.../sqlite-ast/class-wp-sqlite-driver.php | 163 ++---------
wp-includes/sqlite/class-wp-sqlite-db.php | 118 +++++---
5 files changed, 285 insertions(+), 283 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 4805924..c1f4222 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -58,20 +58,9 @@ public function setUp(): void {
);
}
- private function assertQuery( $sql, $error_substring = null ) {
+ private function assertQuery( $sql ) {
$retval = $this->engine->query( $sql );
- if ( null === $error_substring ) {
- $this->assertEquals(
- '',
- $this->engine->get_error_message()
- );
- $this->assertNotFalse(
- $retval
- );
- } else {
- $this->assertStringContainsStringIgnoringCase( $error_substring, $this->engine->get_error_message() );
- }
-
+ $this->assertNotFalse( $retval );
return $retval;
}
@@ -854,7 +843,6 @@ public function testCreateTable() {
KEY user_email (user_email)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci"
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
$this->assertQuery( 'DESCRIBE wptests_users;' );
@@ -953,7 +941,6 @@ public function testCreateTableWithTrailingComma() {
PRIMARY KEY (ID)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci'
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
}
@@ -964,7 +951,6 @@ public function testCreateTableSpatialIndex() {
UNIQUE KEY (ID)
)'
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
}
@@ -978,7 +964,6 @@ enum_column ENUM('a', 'b', 'c') NOT NULL DEFAULT 'a',
PRIMARY KEY (ID)
)"
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
$this->assertQuery( 'DESCRIBE wptests_users;' );
@@ -1031,7 +1016,6 @@ public function testAlterTableAddAndDropColumn() {
$this->assertNull( $result );
$result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD COLUMN `column` int;' );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
$this->assertQuery( 'DESCRIBE _tmp_table;' );
@@ -1059,7 +1043,6 @@ public function testAlterTableAddAndDropColumn() {
);
$result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD `column2` int;' );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
$this->assertQuery( 'DESCRIBE _tmp_table;' );
@@ -1095,7 +1078,6 @@ public function testAlterTableAddAndDropColumn() {
);
$result = $this->assertQuery( 'ALTER TABLE _tmp_table DROP COLUMN `column`;' );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
$this->assertQuery( 'DESCRIBE _tmp_table;' );
@@ -1123,7 +1105,6 @@ public function testAlterTableAddAndDropColumn() {
);
$result = $this->assertQuery( 'ALTER TABLE _tmp_table DROP `column2`;' );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
$this->assertQuery( 'DESCRIBE _tmp_table;' );
@@ -1151,7 +1132,6 @@ public function testAlterTableAddNotNullVarcharColumn() {
);
$result = $this->assertQuery( "ALTER TABLE _tmp_table ADD COLUMN `column` VARCHAR(20) NOT NULL DEFAULT 'foo';" );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
$this->assertQuery( 'DESCRIBE _tmp_table;' );
@@ -1732,7 +1712,6 @@ public function testAlterTableAddIndex() {
);
$result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD INDEX name (name);' );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
$this->assertQuery( 'SHOW INDEX FROM _tmp_table;' );
@@ -1769,7 +1748,6 @@ public function testAlterTableAddUniqueIndex() {
);
$result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD UNIQUE INDEX name (name(20));' );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
$this->assertQuery( 'SHOW INDEX FROM _tmp_table;' );
@@ -1806,7 +1784,6 @@ public function testAlterTableAddFulltextIndex() {
);
$result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD FULLTEXT INDEX name (name);' );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
$this->assertQuery( 'SHOW INDEX FROM _tmp_table;' );
@@ -1850,16 +1827,25 @@ public function testAlterTableModifyColumn() {
$this->assertEquals( 1, $result );
// Primary key violation:
- $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Mike', 'Pearseed');" );
- $this->assertEquals( false, $result );
+ $error = '';
+ try {
+ $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Mike', 'Pearseed')" );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.ID', $error );
// Unique constraint violation:
- $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (2, 'Johnny', 'Appleseed');" );
- $this->assertEquals( false, $result );
+ $error = '';
+ try {
+ $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (2, 'Johnny', 'Appleseed')" );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.name', $error );
// Rename the "name" field to "firstname":
$result = $this->engine->query( "ALTER TABLE _tmp_table CHANGE column name firstname varchar(50) NOT NULL default 'mark';" );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
// Confirm the original data is still there:
@@ -1870,12 +1856,22 @@ public function testAlterTableModifyColumn() {
$this->assertEquals( 'Appleseed', $result[0]->lastname );
// Confirm the primary key is intact:
- $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (1, 'Mike', 'Pearseed');" );
- $this->assertEquals( false, $result );
+ $error = '';
+ try {
+ $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (1, 'Mike', 'Pearseed')" );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.ID', $error );
// Confirm the unique key is intact:
- $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (2, 'Johnny', 'Appleseed');" );
- $this->assertEquals( false, $result );
+ $error = '';
+ try {
+ $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (2, 'Johnny', 'Appleseed')" );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.firstname', $error );
// Confirm the autoincrement still works:
$result = $this->engine->query( "INSERT INTO _tmp_table (firstname, lastname) VALUES ('John', 'Doe');" );
@@ -1901,16 +1897,25 @@ public function testAlterTableModifyColumnWithSkippedColumnKeyword() {
$this->assertEquals( 1, $result );
// Primary key violation:
- $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Mike', 'Pearseed');" );
- $this->assertEquals( false, $result );
+ $error = '';
+ try {
+ $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (1, 'Mike', 'Pearseed')" );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.ID', $error );
// Unique constraint violation:
- $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (2, 'Johnny', 'Appleseed');" );
- $this->assertEquals( false, $result );
+ $error = '';
+ try {
+ $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (2, 'Johnny', 'Appleseed')" );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.name', $error );
// Rename the "name" field to "firstname":
$result = $this->engine->query( "ALTER TABLE _tmp_table CHANGE name firstname varchar(50) NOT NULL default 'mark';" );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
// Confirm the original data is still there:
@@ -1921,12 +1926,22 @@ public function testAlterTableModifyColumnWithSkippedColumnKeyword() {
$this->assertEquals( 'Appleseed', $result[0]->lastname );
// Confirm the primary key is intact:
- $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (1, 'Mike', 'Pearseed');" );
- $this->assertEquals( false, $result );
+ $error = '';
+ try {
+ $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (1, 'Mike', 'Pearseed')" );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.ID', $error );
// Confirm the unique key is intact:
- $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (2, 'Johnny', 'Appleseed');" );
- $this->assertEquals( false, $result );
+ $error = '';
+ try {
+ $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname) VALUES (2, 'Johnny', 'Appleseed')" );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.firstname', $error );
// Confirm the autoincrement still works:
$result = $this->engine->query( "INSERT INTO _tmp_table (firstname, lastname) VALUES ('John', 'Doe');" );
@@ -1942,17 +1957,14 @@ public function testAlterTableModifyColumnWithHyphens() {
`foo-bar` varchar(255) DEFAULT NULL
)'
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
$result = $this->assertQuery(
'ALTER TABLE wptests_dbdelta_test2 CHANGE COLUMN `foo-bar` `foo-bar` text DEFAULT NULL'
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
$result = $this->assertQuery( 'DESCRIBE wptests_dbdelta_test2;' );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNotFalse( $result );
$this->assertEquals(
array(
@@ -1979,21 +1991,18 @@ public function testAlterTableModifyColumnComplexChange() {
PRIMARY KEY (ID, name)
);"
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
// Add a unique index
$result = $this->assertQuery(
'ALTER TABLE _tmp_table ADD UNIQUE INDEX "test_unique_composite" (name, lastname);'
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
// Add a regular index
$result = $this->assertQuery(
'ALTER TABLE _tmp_table ADD INDEX "test_regular" (lastname);'
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
// Confirm the table is well-behaved so far:
@@ -2012,12 +2021,22 @@ public function testAlterTableModifyColumnComplexChange() {
$this->assertEquals( 4, $result );
// Primary key violation:
- $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name) VALUES (1, 'Johnny');" );
- $this->assertEquals( false, $result );
+ $error = '';
+ try {
+ $this->engine->query( "INSERT INTO _tmp_table (ID, name) VALUES (1, 'Johnny')" );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.ID, _tmp_table.name', $error );
// Unique constraint violation:
- $result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (5, 'Kate', 'Bar');" );
- $this->assertEquals( false, $result );
+ $error = '';
+ try {
+ $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (5, 'Kate', 'Bar');" );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.name, _tmp_table.lastname', $error );
// No constraint violation:
$result = $this->engine->query( "INSERT INTO _tmp_table (ID, name, lastname) VALUES (5, 'Joanna', 'Bar');" );
@@ -2025,11 +2044,9 @@ public function testAlterTableModifyColumnComplexChange() {
// Now – let's change a few columns:
$result = $this->engine->query( 'ALTER TABLE _tmp_table CHANGE COLUMN name firstname varchar(20)' );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
$result = $this->engine->query( 'ALTER TABLE _tmp_table CHANGE COLUMN date_as_string datetime datetime NOT NULL' );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
// Finally, let's confirm our data is intact and the table is still well-behaved:
@@ -2041,16 +2058,25 @@ public function testAlterTableModifyColumnComplexChange() {
$this->assertEquals( '2002-01-01 12:53:13', $result[0]->datetime );
// Primary key violation:
- $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, datetime) VALUES (1, 'Johnny', '2010-01-01 12:53:13');" );
- $this->assertEquals( false, $result );
+ $error = '';
+ try {
+ $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, datetime) VALUES (1, 'Johnny', '2010-01-01 12:53:13');" );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.ID, _tmp_table.firstname', $error );
// Unique constraint violation:
- $result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname, datetime) VALUES (6, 'Kate', 'Bar', '2010-01-01 12:53:13');" );
- $this->assertEquals( false, $result );
+ $error = '';
+ try {
+ $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname, datetime) VALUES (6, 'Kate', 'Bar', '2010-01-01 12:53:13');" );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.firstname, _tmp_table.lastname', $error );
// No constraint violation:
$result = $this->engine->query( "INSERT INTO _tmp_table (ID, firstname, lastname, datetime) VALUES (6, 'Sophie', 'Bar', '2010-01-01 12:53:13');" );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertEquals( 1, $result );
}
@@ -2074,12 +2100,15 @@ public function testCaseInsensitiveUniqueIndex() {
$this->assertEquals( 1, $result1[0]->num );
// Unique keys should be case-insensitive:
- $result2 = $this->assertQuery(
- "INSERT INTO _tmp_table (name, lastname) VALUES ('FIRST', 'LAST' );",
- 'UNIQUE constraint failed'
- );
-
- $this->assertEquals( false, $result2 );
+ $error = '';
+ try {
+ $this->assertQuery(
+ "INSERT INTO _tmp_table (name, lastname) VALUES ('FIRST', 'LAST' )"
+ );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed', $error );
$result1 = $this->engine->query( 'SELECT COUNT(*) num FROM _tmp_table;' );
$this->assertEquals( 1, $result1[0]->num );
@@ -2118,7 +2147,6 @@ public function testOnDuplicateUpdate() {
);
$result1 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertEquals( 1, $result1 );
$result2 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST') ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);" );
@@ -2159,7 +2187,6 @@ public function testCaseInsensitiveSelect() {
"INSERT INTO _tmp_table (name) VALUES ('first');"
);
$this->assertQuery( "SELECT name FROM _tmp_table WHERE name = 'FIRST';" );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertCount( 1, $this->engine->get_query_results() );
$this->assertEquals(
array(
@@ -2299,14 +2326,20 @@ public function testNestedTransactionWorkComplexModify() {
// Behind the scenes, this single MySQL query is split
// into multiple SQLite queries – some of them will
// succeed, some will fail.
- $success = $this->engine->query(
+ $error = '';
+ try {
+ $this->engine->query(
+ '
+ ALTER TABLE _options
+ ADD COLUMN test varchar(20),
+ ADD COLUMN test varchar(20)
'
- ALTER TABLE _options
- ADD COLUMN test varchar(20),
- ADD COLUMN test varchar(20)
- '
- );
- $this->assertFalse( $success );
+ );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'duplicate column name: test', $error );
+
// Commit the transaction.
$this->assertQuery( 'COMMIT' );
@@ -2525,11 +2558,11 @@ public function testCreateTableCompositePk() {
KEY term_taxonomy_id (term_taxonomy_id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci'
);
- $result1 = $this->engine->query( 'INSERT INTO wptests_term_relationships VALUES (1,2,1),(1,3,2);' );
- $this->assertEquals( 2, $result1 );
+ $result = $this->engine->query( 'INSERT INTO wptests_term_relationships VALUES (1,2,1),(1,3,2);' );
+ $this->assertEquals( 2, $result );
- $result2 = $this->engine->query( 'INSERT INTO wptests_term_relationships VALUES (1,2,2),(1,3,1);' );
- $this->assertEquals( false, $result2 );
+ $this->expectExceptionMessage( 'UNIQUE constraint failed: wptests_term_relationships.object_id, wptests_term_relationships.term_taxonomy_id' );
+ $this->engine->query( 'INSERT INTO wptests_term_relationships VALUES (1,2,2),(1,3,1);' );
}
public function testDescribeAccurate() {
@@ -2544,11 +2577,9 @@ public function testDescribeAccurate() {
FULLTEXT KEY term_name (term_name)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci'
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNotFalse( $result );
$result = $this->assertQuery( 'DESCRIBE wptests_term_relationships;' );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNotFalse( $result );
$fields = $this->engine->get_query_results();
@@ -2590,15 +2621,12 @@ public function testAlterTableAddColumnChangesMySQLDataType() {
object_id bigint(20) unsigned NOT NULL default 0
)'
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNotFalse( $result );
$result = $this->assertQuery( "ALTER TABLE `_test` ADD COLUMN object_name varchar(255) NOT NULL DEFAULT 'adb';" );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNotFalse( $result );
$result = $this->assertQuery( 'DESCRIBE _test;' );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNotFalse( $result );
$fields = $this->engine->get_query_results();
@@ -2651,7 +2679,6 @@ public function testShowIndex() {
KEY compound_key (object_id,term_taxonomy_id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci'
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNotFalse( $result );
$result = $this->assertQuery( 'SHOW INDEX FROM wptests_term_relationships;' );
@@ -2818,15 +2845,12 @@ public function testInsertOnDuplicateKeyCompositePk() {
KEY term_taxonomy_id (term_taxonomy_id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci'
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNotFalse( $result );
$result1 = $this->assertQuery( 'INSERT INTO wptests_term_relationships VALUES (1,2,1),(1,3,2);' );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertEquals( 2, $result1 );
$result2 = $this->assertQuery( 'INSERT INTO wptests_term_relationships VALUES (1,2,2),(1,3,1) ON DUPLICATE KEY UPDATE term_order = VALUES(term_order);' );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertEquals( 2, $result2 );
$this->assertQuery( 'SELECT COUNT(*) as cnt FROM wptests_term_relationships' );
@@ -2865,19 +2889,16 @@ public function testCalcFoundRows() {
user_login TEXT NOT NULL default ''
);"
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNotFalse( $result );
$result = $this->assertQuery(
"INSERT INTO wptests_dummy (user_login) VALUES ('test1');"
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertEquals( 1, $result );
$result = $this->assertQuery(
"INSERT INTO wptests_dummy (user_login) VALUES ('test2');"
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertEquals( 1, $result );
$result = $this->assertQuery(
@@ -2885,13 +2906,11 @@ public function testCalcFoundRows() {
);
$this->assertNotFalse( $result );
$this->assertCount( 1, $result );
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertEquals( 'test1', $result[0]->user_login );
$result = $this->assertQuery(
'SELECT FOUND_ROWS()'
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertEquals(
array(
(object) array(
@@ -3053,9 +3072,10 @@ public function testCreateTableIfNotExists(): void {
$this->assertQuery(
'CREATE TABLE IF NOT EXISTS t (ID INTEGER, name TEXT)'
);
+
+ $this->expectExceptionMessage( 'table `t` already exists' );
$this->assertQuery(
- 'CREATE TABLE t (ID INTEGER, name TEXT)',
- 'table `t` already exists'
+ 'CREATE TABLE t (ID INTEGER, name TEXT)'
);
}
@@ -3095,7 +3115,6 @@ public function testTranslatesDoubleAlterTable() {
ADD INDEX test_index2(option_name(140),option_value(51))
'
);
- $this->assertEquals( '', $this->engine->get_error_message() );
$this->assertNull( $result );
$result = $this->assertQuery(
@@ -3361,15 +3380,25 @@ public function testUniqueConstraints() {
);
// This should fail because of the UNIQUE constraints.
- $this->assertQuery(
- "UPDATE _tmp_table SET unique_name = 'unique-default-value' WHERE ID = 2",
- 'UNIQUE constraint failed: _tmp_table.unique_name'
- );
+ $error = '';
+ try {
+ $this->assertQuery(
+ "UPDATE _tmp_table SET unique_name = 'unique-default-value' WHERE ID = 2"
+ );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.unique_name', $error );
- $this->assertQuery(
- "UPDATE _tmp_table SET inline_unique_name = 'inline-unique-default-value' WHERE ID = 2",
- 'UNIQUE constraint failed: _tmp_table.inline_unique_name'
- );
+ $error = '';
+ try {
+ $this->assertQuery(
+ "UPDATE _tmp_table SET inline_unique_name = 'inline-unique-default-value' WHERE ID = 2"
+ );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.inline_unique_name', $error );
// Updating "name" to the same value as the first row should pass.
$this->assertQuery(
@@ -3388,10 +3417,15 @@ public function testUniqueConstraints() {
);
// Updating also "unique_name" should fail on the compound UNIQUE key.
- $this->assertQuery(
- "UPDATE _tmp_table SET inline_unique_name = 'inline-unique-default-value' WHERE ID = 2",
- 'UNIQUE constraint failed: _tmp_table.inline_unique_name'
- );
+ $error = '';
+ try {
+ $this->assertQuery(
+ "UPDATE _tmp_table SET inline_unique_name = 'inline-unique-default-value' WHERE ID = 2"
+ );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
+ $this->assertStringContainsString( 'UNIQUE constraint failed: _tmp_table.inline_unique_name', $error );
$result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE ID = 2' );
$this->assertEquals(
@@ -3684,10 +3718,8 @@ public function testSelectNonExistentColumn(): void {
* See:
* https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted
*/
- $this->assertQuery(
- 'SELECT non_existent_column FROM t LIMIT 0',
- 'no such column: non_existent_column'
- );
+ $this->expectExceptionMessage( 'no such column: non_existent_column' );
+ $this->assertQuery( 'SELECT non_existent_column FROM t LIMIT 0' );
}
public function testUnion(): void {
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 0517548..21df067 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -1249,12 +1249,16 @@ public function testConcatFunction(): void {
}
private function assertQuery( $expected, string $query ): void {
- $this->driver->query( $query );
+ $error = null;
+ try {
+ $this->driver->query( $query );
+ } catch ( Throwable $e ) {
+ $error = $e->getMessage();
+ }
// Check for SQLite syntax errors.
// This ensures that invalid SQLite syntax will always fail, even if it
// was the expected result. It prevents us from using wrong assertions.
- $error = $this->driver->get_error_message();
if ( $error && preg_match( '/(SQLSTATE\[HY000].+syntax error\.)/i', $error, $matches ) ) {
$this->fail(
sprintf( "SQLite syntax error: %s\nMySQL query: %s", $matches[1], $query )
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php
index 774d1ee..d14a391 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php
@@ -1,3 +1,24 @@
driver = $driver;
+ }
+
+ public function getDriver(): WP_SQLite_Driver {
+ return $this->driver;
+ }
+}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 977d1cf..8adc92c 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -244,13 +244,6 @@ class WP_SQLite_Driver {
*/
private $pdo;
- /**
- * Whether a debug mode is enabled.
- *
- * @var bool
- */
- private $debug;
-
/**
* @var WP_SQLite_Information_Schema_Builder
*/
@@ -291,20 +284,6 @@ class WP_SQLite_Driver {
*/
private $last_sql_calc_found_rows = null;
- /**
- * Error encountered during the last query.
- *
- * @var array
- */
- private $errors;
-
- /**
- * Error messages produced by the last query.
- *
- * @var array
- */
- private $error_messages = array();
-
/**
* Transaction nesting level of the executed SQLite queries.
*
@@ -334,7 +313,6 @@ class WP_SQLite_Driver {
* Must be set when PDO instance is not provided.
* @type PDO|null $connection Optional. PDO instance with SQLite connection.
* If not provided, a new PDO instance will be created.
- * @type bool $debug Optional. Enable debug mode.
* @type int|null $timeout Optional. SQLite timeout in seconds.
* The time to wait for a writable lock.
* @type string|null $sqlite_journal_mode Optional. SQLite journal mode.
@@ -343,7 +321,7 @@ class WP_SQLite_Driver {
public function __construct( array $options ) {
// Database name.
if ( ! isset( $options['database'] ) || ! is_string( $options['database'] ) ) {
- throw new WP_SQLite_Driver_Exception( 'Option "database" is required.' );
+ throw $this->new_driver_exception( 'Option "database" is required.' );
}
$this->db_name = $options['database'];
@@ -352,13 +330,10 @@ public function __construct( array $options ) {
$this->pdo = $options['connection'];
}
- // Debug mode.
- $this->debug = isset( $options['debug'] ) && true === $options['debug'];
-
// Create a PDO connection if it is not provided.
if ( ! $this->pdo ) {
if ( ! isset( $options['path'] ) || ! is_string( $options['path'] ) ) {
- throw new WP_SQLite_Driver_Exception(
+ throw $this->new_driver_exception(
'Option "path" is required when "connection" is not provided.'
);
}
@@ -367,13 +342,8 @@ public function __construct( array $options ) {
try {
$this->pdo = new PDO( 'sqlite:' . $path );
} catch ( PDOException $e ) {
- $this->error_messages[] = sprintf(
- '%s
%s
%s
',
- 'Database initialization error!',
- 'Code: ' . $e->getCode(),
- 'Error Message: ' . $e->getMessage()
- );
- return;
+ $code = $e->getCode();
+ throw $this->new_driver_exception( $e->getMessage(), is_int( $code ) ? $code : 0, $e );
}
}
@@ -499,7 +469,7 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
$ast = $parser->parse();
if ( null === $ast ) {
- throw new WP_SQLite_Driver_Exception( 'Failed to parse the MySQL query.' );
+ throw $this->new_driver_exception( 'Failed to parse the MySQL query.' );
}
// Handle transaction commands.
@@ -557,10 +527,8 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
} catch ( Throwable $rollback_exception ) {
// Ignore rollback errors.
}
- if ( true === $this->debug ) {
- throw $e;
- }
- return $this->handle_error( $e );
+ $code = $e->getCode();
+ throw $this->new_driver_exception( $e->getMessage(), is_int( $code ) ? $code : 0, $e );
}
}
@@ -606,53 +574,6 @@ public function execute_sqlite_query( $sql, $params = array() ) {
return $stmt;
}
- /**
- * Method to return error messages.
- *
- * @throws Exception If error is found.
- *
- * @return string
- */
- public function get_error_message() {
- if ( count( $this->error_messages ) === 0 ) {
- $this->error_messages = array();
- return '';
- }
-
- $output = '
' . PHP_EOL;
- $output .= '' . PHP_EOL;
- $output .= '
MySQL query:
' . PHP_EOL;
- $output .= '
' . $this->last_mysql_query . '
' . PHP_EOL;
- $output .= '
Queries made or created this session were:
' . PHP_EOL;
- $output .= '
' . PHP_EOL;
- foreach ( $this->last_sqlite_queries as $q ) {
- $message = "Executing: {$q['sql']} | " . ( $q['params'] ? 'parameters: ' . implode( ', ', $q['params'] ) : '(no parameters)' );
-
- $output .= '- ' . htmlspecialchars( $message ) . '
' . PHP_EOL;
- }
- $output .= '
' . PHP_EOL;
- $output .= '
' . PHP_EOL;
- foreach ( $this->error_messages as $num => $m ) {
- $output .= '' . PHP_EOL;
- $output .= sprintf(
- 'Error occurred at line %1$d in Function %2$s. Error message was: %3$s.',
- (int) $this->errors[ $num ]['line'],
- '' . htmlspecialchars( $this->errors[ $num ]['function'] ) . '
',
- $m
- ) . PHP_EOL;
- $output .= '
' . PHP_EOL;
- }
-
- try {
- throw new Exception();
- } catch ( Exception $e ) {
- $output .= 'Backtrace:
' . PHP_EOL;
- $output .= '' . $e->getTraceAsString() . '
' . PHP_EOL;
- }
-
- return $output;
- }
-
/**
* Begin a new transaction or nested transaction.
*/
@@ -706,14 +627,14 @@ public function rollback(): void {
*/
private function execute_mysql_query( WP_Parser_Node $ast ) {
if ( 'query' !== $ast->rule_name ) {
- throw new WP_SQLite_Driver_Exception(
+ throw $this->new_driver_exception(
sprintf( 'Expected "query" node, got: "%s"', $ast->rule_name )
);
}
$children = $ast->get_child_nodes();
if ( count( $children ) !== 1 ) {
- throw new WP_SQLite_Driver_Exception(
+ throw $this->new_driver_exception(
sprintf( 'Expected 1 child, got: %d', count( $children ) )
);
}
@@ -740,7 +661,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$this->execute_create_table_statement( $ast );
break;
default:
- throw $this->not_supported_exception(
+ throw $this->new_not_supported_exception(
sprintf(
'statement type: "%s" > "%s"',
$ast->rule_name,
@@ -756,7 +677,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$this->execute_alter_table_statement( $ast );
break;
default:
- throw $this->not_supported_exception(
+ throw $this->new_not_supported_exception(
sprintf(
'statement type: "%s" > "%s"',
$ast->rule_name,
@@ -794,7 +715,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$this->execute_describe_statement( $subtree );
break;
default:
- throw $this->not_supported_exception(
+ throw $this->new_not_supported_exception(
sprintf(
'statement type: "%s" > "%s"',
$ast->rule_name,
@@ -804,7 +725,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
}
break;
default:
- throw $this->not_supported_exception(
+ throw $this->new_not_supported_exception(
sprintf( 'statement type: "%s"', $ast->rule_name )
);
}
@@ -1283,7 +1204,7 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
$this->last_result = true;
return;
default:
- throw $this->not_supported_exception(
+ throw $this->new_not_supported_exception(
sprintf(
'statement type: "%s" > "%s"',
$node->rule_name,
@@ -1467,7 +1388,7 @@ private function translate( $ast ) {
}
if ( ! $ast instanceof WP_Parser_Node ) {
- throw new WP_SQLite_Driver_Exception(
+ throw $this->new_driver_exception(
sprintf(
'Expected a WP_Parser_Node or WP_MySQL_Token instance, got: %s',
gettype( $ast )
@@ -1519,7 +1440,7 @@ private function translate( $ast ) {
}
if ( null === $child ) {
- throw $this->invalid_input_exception();
+ throw $this->new_invalid_input_exception();
}
$type = self::DATA_TYPE_MAP[ $child->id ] ?? null;
@@ -1533,7 +1454,7 @@ private function translate( $ast ) {
}
// @TODO: Handle SET and JSON.
- throw $this->not_supported_exception(
+ throw $this->new_not_supported_exception(
sprintf( 'data type: %s', $child->value )
);
case 'fromClause':
@@ -1866,7 +1787,7 @@ private function translate_function_call( WP_Parser_Node $node ): string {
$format = strtr( $mysql_format, self::DATE_FORMAT_TO_STRFTIME_MAP );
if ( ! $format ) {
- throw new WP_SQLite_Driver_Exception(
+ throw $this->new_driver_exception(
sprintf(
'Could not translate a DATE_FORMAT() format to STRFTIME format (%s)',
$mysql_format
@@ -1995,7 +1916,7 @@ private function get_sqlite_create_table_statement( string $table_name, ?string
)->fetch( PDO::FETCH_ASSOC );
if ( false === $table_info ) {
- throw new WP_SQLite_Driver_Exception(
+ throw $this->new_driver_exception(
sprintf( 'Table "%s" not found in information schema', $table_name )
);
}
@@ -2340,7 +2261,6 @@ private function flush() {
$this->last_sqlite_queries = array();
$this->last_result = null;
$this->last_return_value = null;
- $this->error_messages = array();
}
/**
@@ -2374,46 +2294,21 @@ private function set_result_from_affected_rows( $override = null ) {
$this->last_return_value = $affected_rows;
}
- /**
- * Error handler.
- *
- * @param Exception $err Exception object.
- *
- * @return bool Always false.
- */
- private function handle_error( Exception $err ) {
- $message = $err->getMessage();
- $this->set_error( __LINE__, __FUNCTION__, $message );
- $this->last_return_value = false;
- return false;
- }
-
- /**
- * Method to format the error messages and put out to the file.
- *
- * When $wpdb::suppress_errors is set to true or $wpdb::show_errors is set to false,
- * the error messages are ignored.
- *
- * @param string $line Where the error occurred.
- * @param string $function_name Indicate the function name where the error occurred.
- * @param string $message The message.
- *
- * @return boolean|void
- */
- private function set_error( $line, $function_name, $message ) {
- $this->errors[] = array(
- 'line' => $line,
- 'function' => $function_name,
- );
- $this->error_messages[] = $message;
+ private function new_driver_exception(
+ string $message,
+ int $code = 0,
+ Throwable $previous = null
+ ): WP_SQLite_Driver_Exception {
+ return new WP_SQLite_Driver_Exception( $this, $message, $code, $previous );
}
- private function invalid_input_exception() {
- throw new WP_SQLite_Driver_Exception( 'MySQL query syntax error.' );
+ private function new_invalid_input_exception(): WP_SQLite_Driver_Exception {
+ throw new WP_SQLite_Driver_Exception( $this, 'MySQL query syntax error.' );
}
- private function not_supported_exception( string $cause ): Exception {
+ private function new_not_supported_exception( string $cause ): WP_SQLite_Driver_Exception {
return new WP_SQLite_Driver_Exception(
+ $this,
sprintf( 'MySQL query not supported. Cause: %s', $cause )
);
}
diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php
index 401d386..a958625 100644
--- a/wp-includes/sqlite/class-wp-sqlite-db.php
+++ b/wp-includes/sqlite/class-wp-sqlite-db.php
@@ -125,25 +125,22 @@ public function esc_like( $text ) {
}
/**
- * Method to put out the error message.
+ * Prints SQL/DB error.
*
- * This overrides wpdb::print_error(), for we can't use the parent class method.
- *
- * @see wpdb::print_error()
+ * This overrides wpdb::print_error() while closely mirroring its implementation.
*
* @global array $EZSQL_ERROR Stores error information of query and error string.
*
* @param string $str The error to display.
- *
- * @return bool|void False if the showing of errors is disabled.
+ * @return void|false Void if the showing of errors is enabled, false if disabled.
*/
public function print_error( $str = '' ) {
global $EZSQL_ERROR;
if ( ! $str ) {
- $err = $this->dbh->get_error_message() ? $this->dbh->get_error_message() : '';
- $str = empty( $err ) ? '' : $err[2];
+ $str = $this->last_error;
}
+
$EZSQL_ERROR[] = array(
'query' => $this->last_query,
'error_str' => $str,
@@ -153,26 +150,32 @@ public function print_error( $str = '' ) {
return false;
}
- wp_load_translations_early();
-
$caller = $this->get_caller();
- $caller = $caller ? $caller : '(unknown)';
-
- $error_str = sprintf(
- 'WordPress database error %1$s for query %2$s made by %3$s',
- $str,
- $this->last_query,
- $caller
- );
+ if ( $caller ) {
+ // Not translated, as this will only appear in the error log.
+ $error_str = sprintf( 'WordPress database error %1$s for query %2$s made by %3$s', $str, $this->last_query, $caller );
+ } else {
+ $error_str = sprintf( 'WordPress database error %1$s for query %2$s', $str, $this->last_query );
+ }
error_log( $error_str );
+ // Are we showing errors?
if ( ! $this->show_errors ) {
return false;
}
+ wp_load_translations_early();
+
+ // If there is an error then take note of it.
if ( is_multisite() ) {
- $msg = "WordPress database error: [$str]\n{$this->last_query}\n";
+ $msg = sprintf(
+ "%s [%s]\n%s\n",
+ __( 'WordPress database error:' ),
+ $str,
+ $this->last_query
+ );
+
if ( defined( 'ERRORLOGFILE' ) ) {
error_log( $msg, 3, ERRORLOGFILE );
}
@@ -184,9 +187,10 @@ public function print_error( $str = '' ) {
$query = htmlspecialchars( $this->last_query, ENT_QUOTES );
printf(
- 'WordPress database error: [%1$s] %2$s
',
+ '',
+ __( 'WordPress database error:' ),
$str,
- '' . $query . '
'
+ $query
);
}
}
@@ -241,19 +245,23 @@ public function db_connect( $allow_bail = true ) {
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php';
require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
$this->ensure_database_directory( FQDB );
- $this->dbh = new WP_SQLite_Driver(
- array(
- 'connection' => $pdo,
- 'debug' => defined( 'PDO_DEBUG' ) && true === PDO_DEBUG,
- 'path' => FQDB,
- 'database' => $this->dbname,
- 'sqlite_journal_mode' => defined( 'SQLITE_JOURNAL_MODE' ) ? SQLITE_JOURNAL_MODE : null,
- )
- );
+
+ try {
+ $this->dbh = new WP_SQLite_Driver(
+ array(
+ 'connection' => $pdo,
+ 'path' => FQDB,
+ 'database' => $this->dbname,
+ 'sqlite_journal_mode' => defined( 'SQLITE_JOURNAL_MODE' ) ? SQLITE_JOURNAL_MODE : null,
+ )
+ );
+ } catch ( Throwable $e ) {
+ $this->last_error = $this->format_error_message( $e );
+ }
} else {
- $this->dbh = new WP_SQLite_Translator( $pdo );
+ $this->dbh = new WP_SQLite_Translator( $pdo );
+ $this->last_error = $this->dbh->get_error_message();
}
- $this->last_error = $this->dbh->get_error_message();
if ( $this->last_error ) {
return false;
}
@@ -317,7 +325,6 @@ public function query( $query ) {
*/
$this->_do_query( $query );
- $this->last_error = $this->dbh->get_error_message();
if ( $this->last_error ) {
// Clear insert_id on a subsequent failed insert.
if ( $this->insert_id && preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
@@ -374,8 +381,14 @@ private function _do_query( $query ) {
$this->timer_start();
}
- if ( ! empty( $this->dbh ) ) {
+ try {
$this->result = $this->dbh->query( $query );
+ } catch ( Throwable $e ) {
+ $this->last_error = $this->format_error_message( $e );
+ }
+
+ if ( $this->dbh instanceof WP_SQLite_Translator ) {
+ $this->last_error = $this->dbh->get_error_message();
}
++$this->num_queries;
@@ -491,4 +504,41 @@ private function ensure_database_directory( string $database_path ) {
// Restore the original umask value.
umask( $umask );
}
+
+
+ /**
+ * Format SQLite driver error message.
+ *
+ * @return string
+ */
+ private function format_error_message( Throwable $e ) {
+ $output = '
' . PHP_EOL;
+
+ // Queries.
+ if ( $e instanceof WP_SQLite_Driver_Exception ) {
+ $driver = $e->getDriver();
+
+ $output .= '' . PHP_EOL;
+ $output .= '
MySQL query:
' . PHP_EOL;
+ $output .= '
' . $driver->get_last_mysql_query() . '
' . PHP_EOL;
+ $output .= '
Queries made or created this session were:
' . PHP_EOL;
+ $output .= '
' . PHP_EOL;
+ foreach ( $driver->get_last_sqlite_queries() as $q ) {
+ $message = "Executing: {$q['sql']} | " . ( $q['params'] ? 'parameters: ' . implode( ', ', $q['params'] ) : '(no parameters)' );
+ $output .= '- ' . htmlspecialchars( $message ) . '
' . PHP_EOL;
+ }
+ $output .= '
' . PHP_EOL;
+ $output .= '
' . PHP_EOL;
+ }
+
+ // Message.
+ $output .= '' . PHP_EOL;
+ $output .= $e->getMessage() . PHP_EOL;
+ $output .= '
' . PHP_EOL;
+
+ // Backtrace.
+ $output .= 'Backtrace:
' . PHP_EOL;
+ $output .= '' . $e->getTraceAsString() . '
' . PHP_EOL;
+ return $output;
+ }
}
From 09fbcd9e1000f1ad391d8d9fd4c427e706f5cac4 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 7 Feb 2025 14:35:33 +0100
Subject: [PATCH 120/124] Improve docs and naming
---
tests/WP_SQLite_Driver_Translation_Tests.php | 2 +-
.../sqlite-ast/class-wp-sqlite-driver.php | 522 +++++++++++++-----
...s-wp-sqlite-information-schema-builder.php | 257 ++++++++-
3 files changed, 637 insertions(+), 144 deletions(-)
diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php
index 21df067..2f1bed3 100644
--- a/tests/WP_SQLite_Driver_Translation_Tests.php
+++ b/tests/WP_SQLite_Driver_Translation_Tests.php
@@ -1285,7 +1285,7 @@ function ( $query ) {
// Remove "select changes()" executed after some queries.
if (
count( $executed_queries ) > 1
- && 'select changes()' === $executed_queries[ count( $executed_queries ) - 1 ] ) {
+ && 'SELECT CHANGES()' === $executed_queries[ count( $executed_queries ) - 1 ] ) {
array_pop( $executed_queries );
}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 8adc92c..f58339e 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -18,12 +18,12 @@ class WP_SQLite_Driver {
/**
* The path to the MySQL SQL grammar file.
*/
- const GRAMMAR_PATH = __DIR__ . '/../../wp-includes/mysql/mysql-grammar.php';
+ const MYSQL_GRAMMAR_PATH = __DIR__ . '/../../wp-includes/mysql/mysql-grammar.php';
/**
* The default timeout in seconds for SQLite to wait for a writable lock.
*/
- const DEFAULT_TIMEOUT = 10;
+ const DEFAULT_SQLITE_TIMEOUT = 10;
/**
* An identifier prefix for internal database objects.
@@ -245,6 +245,8 @@ class WP_SQLite_Driver {
private $pdo;
/**
+ * A service for managing MySQL INFORMATION_SCHEMA tables in SQLite.
+ *
* @var WP_SQLite_Information_Schema_Builder
*/
private $information_schema_builder;
@@ -259,7 +261,7 @@ class WP_SQLite_Driver {
/**
* A list of SQLite queries executed for the last MySQL query.
*
- * @var array
+ * @var array{ sql: string, params: array }[]
*/
private $last_sqlite_queries = array();
@@ -317,6 +319,8 @@ class WP_SQLite_Driver {
* The time to wait for a writable lock.
* @type string|null $sqlite_journal_mode Optional. SQLite journal mode.
* }
+ *
+ * @throws WP_SQLite_Driver_Exception When the driver initialization fails.
*/
public function __construct( array $options ) {
// Database name.
@@ -354,7 +358,7 @@ public function __construct( array $options ) {
if ( isset( $options['timeout'] ) && is_int( $options['timeout'] ) ) {
$timeout = $options['timeout'];
} else {
- $timeout = self::DEFAULT_TIMEOUT;
+ $timeout = self::DEFAULT_SQLITE_TIMEOUT;
}
$this->pdo->setAttribute( PDO::ATTR_TIMEOUT, $timeout );
@@ -381,7 +385,7 @@ public function __construct( array $options ) {
// Load MySQL grammar.
if ( null === self::$mysql_grammar ) {
- self::$mysql_grammar = new WP_Parser_Grammar( require self::GRAMMAR_PATH );
+ self::$mysql_grammar = new WP_Parser_Grammar( require self::MYSQL_GRAMMAR_PATH );
}
// Initialize information schema builder.
@@ -400,7 +404,7 @@ public function __construct( array $options ) {
*
* @return PDO
*/
- public function get_pdo() {
+ public function get_pdo(): PDO {
return $this->pdo;
}
@@ -423,16 +427,18 @@ public function get_last_mysql_query(): ?string {
}
/**
- * Get all SQLite queries executed for the last MySQL query.
+ * Get SQLite queries executed for the last MySQL query.
*
- * @return array
+ * @return array{ sql: string, params: array }[]
*/
public function get_last_sqlite_queries(): array {
return $this->last_sqlite_queries;
}
/**
- * Method to return inserted row id.
+ * Get the auto-increment value generated for the last query.
+ *
+ * @return int|string
*/
public function get_insert_id() {
$last_insert_id = $this->pdo->lastInsertId();
@@ -451,9 +457,9 @@ public function get_insert_id() {
* @param int $fetch_mode PDO fetch mode. Default is PDO::FETCH_OBJ.
* @param array ...$fetch_mode_args Additional fetch mode arguments.
*
- * @return mixed Return value, depending on the query type.
- * @throws Exception If the query could not run.
- * @throws PDOException If the translated query could not run.
+ * @return mixed Return value, depending on the query type.
+ *
+ * @throws WP_SQLite_Driver_Exception When the query execution fails.
*/
public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) {
$this->flush();
@@ -473,6 +479,11 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
}
// Handle transaction commands.
+
+ /*
+ * [GRAMMAR]
+ * beginWork: BEGIN_SYMBOL WORK_SYMBOL?
+ */
$child = $ast->get_first_child();
if ( $child instanceof WP_Parser_Node && 'beginWork' === $child->rule_name ) {
$this->begin_transaction();
@@ -480,6 +491,11 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
}
if ( $child instanceof WP_Parser_Node && 'simpleStatement' === $child->rule_name ) {
+ /*
+ * [GRAMMAR]
+ * transactionOrLockingStatement:
+ * transactionStatement | savepointStatement | lockStatement | xaStatement
+ */
$subchild = $child->get_first_child_node( 'transactionOrLockingStatement' );
if ( null !== $subchild ) {
$tokens = $subchild->get_descendant_tokens();
@@ -533,7 +549,7 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
}
/**
- * Method to return the queried result data.
+ * Get results of the last query.
*
* @return mixed
*/
@@ -542,7 +558,7 @@ public function get_query_results() {
}
/**
- * Method to return the queried results according to the query types.
+ * Get return value of the last query() function call.
*
* @return mixed
*/
@@ -551,25 +567,19 @@ public function get_last_return_value() {
}
/**
- * Executes a query in SQLite.
+ * Execute a query in SQLite.
*
- * @param mixed $sql The query to execute.
- * @param mixed $params The parameters to bind to the query.
- * @throws PDOException If the query could not be executed.
- * @return object {
- * The result of the query.
- *
- * @type PDOStatement $stmt The executed statement
- * @type * $result The value returned by $stmt.
- * }
+ * @param string $sql The query to execute.
+ * @param array $params The query parameters.
+ * @throws PDOException When the query execution fails.
+ * @return PDOStatement The PDO statement object.
*/
- public function execute_sqlite_query( $sql, $params = array() ) {
+ public function execute_sqlite_query( string $sql, array $params = array() ): PDOStatement {
$this->last_sqlite_queries[] = array(
'sql' => $sql,
'params' => $params,
);
-
- $stmt = $this->pdo->prepare( $sql );
+ $stmt = $this->pdo->prepare( $sql );
$stmt->execute( $params );
return $stmt;
}
@@ -619,81 +629,93 @@ public function rollback(): void {
}
/**
- * Executes a MySQL query in SQLite.
- *
- * @param string $query The query.
+ * Translate and execute a MySQL query in SQLite.
*
- * @throws Exception If the query is not supported.
+ * @param WP_Parser_Node $node The "query" AST node with "simpleStatement" child.
+ * @throws WP_SQLite_Driver_Exception When the query is not supported.
*/
- private function execute_mysql_query( WP_Parser_Node $ast ) {
- if ( 'query' !== $ast->rule_name ) {
+ private function execute_mysql_query( WP_Parser_Node $node ): void {
+ if ( 'query' !== $node->rule_name ) {
throw $this->new_driver_exception(
- sprintf( 'Expected "query" node, got: "%s"', $ast->rule_name )
+ sprintf( 'Expected "query" node, got: "%s"', $node->rule_name )
);
}
- $children = $ast->get_child_nodes();
+ /*
+ * [GRAMMAR]
+ * query:
+ * EOF
+ * | (simpleStatement | beginWork) (SEMICOLON_SYMBOL EOF? | EOF)
+ */
+ $children = $node->get_child_nodes();
if ( count( $children ) !== 1 ) {
throw $this->new_driver_exception(
- sprintf( 'Expected 1 child, got: %d', count( $children ) )
+ sprintf( 'Expected 1 child node, got: %d', count( $children ) )
);
}
- $ast = $children[0]->get_first_child_node();
- switch ( $ast->rule_name ) {
+ if ( 'simpleStatement' !== $children[0]->rule_name ) {
+ throw $this->new_driver_exception(
+ sprintf( 'Expected "simpleStatement" node, got: "%s"', $children[0]->rule_name )
+ );
+ }
+
+ // Process the "simpleStatement" AST node.
+ $node = $children[0]->get_first_child_node();
+ switch ( $node->rule_name ) {
case 'selectStatement':
- $this->execute_select_statement( $ast );
+ $this->execute_select_statement( $node );
break;
case 'insertStatement':
case 'replaceStatement':
- $this->execute_insert_or_replace_statement( $ast );
+ $this->execute_insert_or_replace_statement( $node );
break;
case 'updateStatement':
- $this->execute_update_statement( $ast );
+ $this->execute_update_statement( $node );
break;
case 'deleteStatement':
- $this->execute_delete_statement( $ast );
+ $this->execute_delete_statement( $node );
break;
case 'createStatement':
- $subtree = $ast->get_first_child_node();
+ $subtree = $node->get_first_child_node();
switch ( $subtree->rule_name ) {
case 'createTable':
- $this->execute_create_table_statement( $ast );
+ $this->execute_create_table_statement( $node );
break;
default:
throw $this->new_not_supported_exception(
sprintf(
'statement type: "%s" > "%s"',
- $ast->rule_name,
+ $node->rule_name,
$subtree->rule_name
)
);
}
break;
case 'alterStatement':
- $subtree = $ast->get_first_child_node();
+ $subtree = $node->get_first_child_node();
switch ( $subtree->rule_name ) {
case 'alterTable':
- $this->execute_alter_table_statement( $ast );
+ $this->execute_alter_table_statement( $node );
break;
default:
throw $this->new_not_supported_exception(
sprintf(
'statement type: "%s" > "%s"',
- $ast->rule_name,
+ $node->rule_name,
$subtree->rule_name
)
);
}
break;
case 'dropStatement':
- $subtree = $ast->get_first_child_node();
+ $subtree = $node->get_first_child_node();
switch ( $subtree->rule_name ) {
case 'dropTable':
- $this->execute_drop_table_statement( $ast );
+ $this->execute_drop_table_statement( $node );
break;
default:
- $query = $this->translate( $ast );
+ $query = $this->translate( $node );
$this->execute_sqlite_query( $query );
$this->set_result_from_affected_rows();
}
@@ -706,10 +728,10 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
$this->last_result = 0;
break;
case 'showStatement':
- $this->execute_show_statement( $ast );
+ $this->execute_show_statement( $node );
break;
case 'utilityStatement':
- $subtree = $ast->get_first_child_node();
+ $subtree = $node->get_first_child_node();
switch ( $subtree->rule_name ) {
case 'describeStatement':
$this->execute_describe_statement( $subtree );
@@ -718,7 +740,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
throw $this->new_not_supported_exception(
sprintf(
'statement type: "%s" > "%s"',
- $ast->rule_name,
+ $node->rule_name,
$subtree->rule_name
)
);
@@ -726,19 +748,32 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
break;
default:
throw $this->new_not_supported_exception(
- sprintf( 'statement type: "%s"', $ast->rule_name )
+ sprintf( 'statement type: "%s"', $node->rule_name )
);
}
}
+ /**
+ * Translate and execute a MySQL SELECT statement in SQLite.
+ *
+ * @param WP_Parser_Node $node The "selectStatement" AST node.
+ * @throws WP_SQLite_Driver_Exception When the query execution fails.
+ */
private function execute_select_statement( WP_Parser_Node $node ): void {
- $has_sql_calc_found_rows = null !== $node->get_first_descendant_token(
- WP_MySQL_Lexer::SQL_CALC_FOUND_ROWS_SYMBOL
- );
+ /*
+ * [GRAMMAR]
+ * selectStatement:
+ * queryExpression lockingClauseList?
+ * | selectStatementWithInto
+ */
// First, translate the query, before we modify last found rows count.
$query = $this->translate( $node->get_first_child() );
+ $has_sql_calc_found_rows = null !== $node->get_first_descendant_token(
+ WP_MySQL_Lexer::SQL_CALC_FOUND_ROWS_SYMBOL
+ );
+
// Handle SQL_CALC_FOUND_ROWS.
if ( true === $has_sql_calc_found_rows ) {
// Recursively find a query expression with the first LIMIT or SELECT.
@@ -771,6 +806,7 @@ private function execute_select_statement( WP_Parser_Node $node ): void {
break;
}
+ // Exclude the limit clause from the expression.
$count_expr = new WP_Parser_Node( $query_expr->rule_id, $query_expr->rule_name );
foreach ( $query_expr->get_children() as $child ) {
if ( ! ( $child instanceof WP_Parser_Node && 'limitClause' === $child->rule_name ) ) {
@@ -778,7 +814,11 @@ private function execute_select_statement( WP_Parser_Node $node ): void {
}
}
- $result = $this->execute_sqlite_query( 'SELECT COUNT(*) AS cnt FROM (' . $this->translate( $count_expr ) . ')' );
+ // Get count of all the rows.
+ $result = $this->execute_sqlite_query(
+ 'SELECT COUNT(*) AS cnt FROM (' . $this->translate( $count_expr ) . ')'
+ );
+
$this->last_sql_calc_found_rows = $result->fetchColumn();
} else {
$this->last_sql_calc_found_rows = null;
@@ -791,10 +831,17 @@ private function execute_select_statement( WP_Parser_Node $node ): void {
);
}
+ /**
+ * Translate and execute a MySQL INSERT or REPLACE statement in SQLite.
+ *
+ * @param WP_Parser_Node $node The "insertStatement" or "replaceStatement" AST node.
+ * @throws WP_SQLite_Driver_Exception When the query execution fails.
+ */
private function execute_insert_or_replace_statement( WP_Parser_Node $node ): void {
$parts = array();
foreach ( $node->get_children() as $child ) {
if ( $child instanceof WP_MySQL_Token && WP_MySQL_Lexer::IGNORE_SYMBOL === $child->id ) {
+ // Translate "UPDATE IGNORE" to "UPDATE OR IGNORE".
$parts[] = 'OR IGNORE';
} else {
$parts[] = $this->translate( $child );
@@ -805,6 +852,12 @@ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): vo
$this->set_result_from_affected_rows();
}
+ /**
+ * Translate and execute a MySQL UPDATE statement in SQLite.
+ *
+ * @param WP_Parser_Node $node The "updateStatement" AST node.
+ * @throws WP_SQLite_Driver_Exception When the query execution fails.
+ */
private function execute_update_statement( WP_Parser_Node $node ): void {
// @TODO: Add support for UPDATE with multiple tables and JOINs.
// SQLite supports them in the FROM clause.
@@ -833,6 +886,7 @@ private function execute_update_statement( WP_Parser_Node $node ): void {
);
}
+ // Iterate and translate the update statement children.
$parts = array();
foreach ( $node->get_children() as $child ) {
if ( $child instanceof WP_MySQL_Token && WP_MySQL_Lexer::IGNORE_SYMBOL === $child->id ) {
@@ -854,6 +908,7 @@ private function execute_update_statement( WP_Parser_Node $node ): void {
}
}
+ // Compose the update query.
$query = implode( ' ', $parts );
if ( null !== $where_subquery ) {
$query .= ' WHERE rowid IN ( ' . $where_subquery . ' )';
@@ -863,6 +918,12 @@ private function execute_update_statement( WP_Parser_Node $node ): void {
$this->set_result_from_affected_rows();
}
+ /**
+ * Translate and execute a MySQL DELETE statement in SQLite.
+ *
+ * @param WP_Parser_Node $node The "deleteStatement" AST node.
+ * @throws WP_SQLite_Driver_Exception When the query execution fails.
+ */
private function execute_delete_statement( WP_Parser_Node $node ): void {
/*
* Multi-table DELETE.
@@ -947,8 +1008,16 @@ private function execute_delete_statement( WP_Parser_Node $node ): void {
$this->set_result_from_affected_rows();
}
+ /**
+ * Translate and execute a MySQL CREATE TABLE statement in SQLite.
+ *
+ * @param WP_Parser_Node $node The "createStatement" AST node with "createTable" child.
+ * @throws WP_SQLite_Driver_Exception When the query execution fails.
+ */
private function execute_create_table_statement( WP_Parser_Node $node ): void {
- $subnode = $node->get_first_child_node();
+ $subnode = $node->get_first_child_node();
+
+ // Handle TEMPORARY and CREATE TABLE ... SELECT.
$is_temporary = $subnode->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL );
$element_list = $subnode->get_first_child_node( 'tableElementList' );
if ( true === $is_temporary || null === $element_list ) {
@@ -958,10 +1027,12 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void {
return;
}
+ // Get table name.
$table_name = $this->unquote_sqlite_identifier(
$this->translate( $subnode->get_first_child_node( 'tableName' ) )
);
+ // Handle IF NOT EXISTS.
if ( $subnode->has_child_node( 'ifNotExists' ) ) {
$table_exists = $this->execute_sqlite_query(
'SELECT 1 FROM _mysql_information_schema_tables WHERE table_schema = ? AND table_name = ?',
@@ -989,6 +1060,12 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void {
}
}
+ /**
+ * Translate and execute a MySQL ALTER TABLE statement in SQLite.
+ *
+ * @param WP_Parser_Node $node The "alterStatement" AST node with "alterTable" child.
+ * @throws WP_SQLite_Driver_Exception When the query execution fails.
+ */
private function execute_alter_table_statement( WP_Parser_Node $node ): void {
$table_name = $this->unquote_sqlite_identifier(
$this->translate( $node->get_first_descendant_node( 'tableRef' ) )
@@ -1110,6 +1187,12 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void {
// consist only of operations that SQLite's ALTER TABLE supports.
}
+ /**
+ * Translate and execute a MySQL DROP TABLE statement in SQLite.
+ *
+ * @param WP_Parser_Node $node The "dropStatement" AST node with "dropTable" child.
+ * @throws WP_SQLite_Driver_Exception When the query execution fails.
+ */
private function execute_drop_table_statement( WP_Parser_Node $node ): void {
$child_node = $node->get_first_child_node();
@@ -1150,6 +1233,12 @@ private function execute_drop_table_statement( WP_Parser_Node $node ): void {
$this->information_schema_builder->record_drop_table( $node );
}
+ /**
+ * Translate and execute a MySQL SHOW statement in SQLite.
+ *
+ * @param WP_Parser_Node $node The "showStatement" AST node.
+ * @throws WP_SQLite_Driver_Exception When the query execution fails.
+ */
private function execute_show_statement( WP_Parser_Node $node ): void {
$tokens = $node->get_child_tokens();
$keyword1 = $tokens[1];
@@ -1214,6 +1303,11 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
}
}
+ /**
+ * Translate and execute a MySQL SHOW INDEX statement in SQLite.
+ *
+ * @param string $table_name The table name to show indexes for.
+ */
private function execute_show_index_statement( string $table_name ): void {
$index_info = $this->execute_sqlite_query(
'
@@ -1247,6 +1341,12 @@ private function execute_show_index_statement( string $table_name ): void {
$this->set_results_from_fetched_data( $index_info );
}
+ /**
+ * Translate and execute a MySQL SHOW TABLE STATUS statement in SQLite.
+ *
+ * @param WP_Parser_Node $node The "showStatement" AST node.
+ * @throws WP_SQLite_Driver_Exception When the query execution fails.
+ */
private function execute_show_table_status_statement( WP_Parser_Node $node ): void {
// FROM/IN database.
$in_db = $node->get_first_child_node( 'inDb' );
@@ -1261,7 +1361,7 @@ private function execute_show_table_status_statement( WP_Parser_Node $node ): vo
// LIKE and WHERE clauses.
$like_or_where = $node->get_first_child_node( 'likeOrWhere' );
if ( null !== $like_or_where ) {
- $condition = $this->get_show_like_or_where_condition( $like_or_where );
+ $condition = $this->translate_show_like_or_where_condition( $like_or_where );
}
// Fetch table information.
@@ -1305,6 +1405,12 @@ private function execute_show_table_status_statement( WP_Parser_Node $node ): vo
$this->set_results_from_fetched_data( $tables );
}
+ /**
+ * Translate and execute a MySQL SHOW TABLES statement in SQLite.
+ *
+ * @param WP_Parser_Node $node The "showStatement" AST node.
+ * @throws WP_SQLite_Driver_Exception When the query execution fails.
+ */
private function execute_show_tables_statement( WP_Parser_Node $node ): void {
// FROM/IN database.
$in_db = $node->get_first_child_node( 'inDb' );
@@ -1319,7 +1425,7 @@ private function execute_show_tables_statement( WP_Parser_Node $node ): void {
// LIKE and WHERE clauses.
$like_or_where = $node->get_first_child_node( 'likeOrWhere' );
if ( null !== $like_or_where ) {
- $condition = $this->get_show_like_or_where_condition( $like_or_where );
+ $condition = $this->translate_show_like_or_where_condition( $like_or_where );
}
// Fetch table information.
@@ -1354,6 +1460,12 @@ private function execute_show_tables_statement( WP_Parser_Node $node ): void {
$this->set_results_from_fetched_data( $tables );
}
+ /**
+ * Translate and execute a MySQL DESCRIBE statement in SQLite.
+ *
+ * @param WP_Parser_Node $node The "describeStatement" AST node.
+ * @throws WP_SQLite_Driver_Exception When the query execution fails.
+ */
private function execute_describe_statement( WP_Parser_Node $node ): void {
$table_name = $this->unquote_sqlite_identifier(
$this->translate( $node->get_first_child_node( 'tableRef' ) )
@@ -1378,31 +1490,38 @@ private function execute_describe_statement( WP_Parser_Node $node ): void {
$this->set_results_from_fetched_data( $column_info );
}
- private function translate( $ast ) {
- if ( null === $ast ) {
+ /**
+ * Translate a MySQL AST node or token to an SQLite query fragment.
+ *
+ * @param WP_Parser_Node|WP_MySQL_Token $node The AST node to translate.
+ * @return string|null The translated query fragment.
+ * @throws WP_SQLite_Driver_Exception When the translation fails.
+ */
+ private function translate( $node ): ?string {
+ if ( null === $node ) {
return null;
}
- if ( $ast instanceof WP_MySQL_Token ) {
- return $this->translate_token( $ast );
+ if ( $node instanceof WP_MySQL_Token ) {
+ return $this->translate_token( $node );
}
- if ( ! $ast instanceof WP_Parser_Node ) {
+ if ( ! $node instanceof WP_Parser_Node ) {
throw $this->new_driver_exception(
sprintf(
'Expected a WP_Parser_Node or WP_MySQL_Token instance, got: %s',
- gettype( $ast )
+ gettype( $node )
)
);
}
- $rule_name = $ast->rule_name;
+ $rule_name = $node->rule_name;
switch ( $rule_name ) {
case 'querySpecification':
// Translate "HAVING ..." without "GROUP BY ..." to "GROUP BY 1 HAVING ...".
- if ( $ast->has_child_node( 'havingClause' ) && ! $ast->has_child_node( 'groupByClause' ) ) {
+ if ( $node->has_child_node( 'havingClause' ) && ! $node->has_child_node( 'groupByClause' ) ) {
$parts = array();
- foreach ( $ast->get_children() as $child ) {
+ foreach ( $node->get_children() as $child ) {
if ( $child instanceof WP_Parser_Node && 'havingClause' === $child->rule_name ) {
$parts[] = 'GROUP BY 1';
}
@@ -1413,19 +1532,19 @@ private function translate( $ast ) {
}
return implode( ' ', $parts );
}
- return $this->translate_sequence( $ast->get_children() );
+ return $this->translate_sequence( $node->get_children() );
case 'qualifiedIdentifier':
case 'dotIdentifier':
- return $this->translate_sequence( $ast->get_children(), '' );
+ return $this->translate_sequence( $node->get_children(), '' );
case 'identifierKeyword':
- return '`' . $this->translate( $ast->get_first_child() ) . '`';
+ return '`' . $this->translate( $node->get_first_child() ) . '`';
case 'pureIdentifier':
- return $this->translate_pure_identifier( $ast );
+ return $this->translate_pure_identifier( $node );
case 'textStringLiteral':
- return $this->translate_string_literal( $ast );
+ return $this->translate_string_literal( $node );
case 'dataType':
case 'nchar':
- $child = $ast->get_first_child();
+ $child = $node->get_first_child();
if ( $child instanceof WP_Parser_Node ) {
return $this->translate( $child );
}
@@ -1434,9 +1553,9 @@ private function translate( $ast ) {
// 1. LONG VARCHAR, LONG CHAR(ACTER) VARYING, LONG VARBINARY.
// 2. NATIONAL CHAR, NATIONAL VARCHAR, NATIONAL CHAR(ACTER) VARYING.
if ( WP_MySQL_Lexer::LONG_SYMBOL === $child->id ) {
- $child = $ast->get_child_tokens()[1] ?? null;
+ $child = $node->get_child_tokens()[1] ?? null;
} elseif ( WP_MySQL_Lexer::NATIONAL_SYMBOL === $child->id ) {
- $child = $ast->get_child_tokens()[1] ?? null;
+ $child = $node->get_child_tokens()[1] ?? null;
}
if ( null === $child ) {
@@ -1460,30 +1579,30 @@ private function translate( $ast ) {
case 'fromClause':
// FROM DUAL is MySQL-specific syntax that means "FROM no tables"
// and it is equivalent to omitting the FROM clause entirely.
- if ( $ast->has_child_token( WP_MySQL_Lexer::DUAL_SYMBOL ) ) {
+ if ( $node->has_child_token( WP_MySQL_Lexer::DUAL_SYMBOL ) ) {
return null;
}
- return $this->translate_sequence( $ast->get_children() );
+ return $this->translate_sequence( $node->get_children() );
case 'insertUpdateList':
// Translate "ON DUPLICATE KEY UPDATE" to "ON CONFLICT DO UPDATE SET".
return sprintf(
'ON CONFLICT DO UPDATE SET %s',
- $this->translate( $ast->get_first_child_node( 'updateList' ) )
+ $this->translate( $node->get_first_child_node( 'updateList' ) )
);
case 'simpleExpr':
- return $this->translate_simple_expr( $ast );
+ return $this->translate_simple_expr( $node );
case 'predicateOperations':
- $token = $ast->get_first_child_token();
+ $token = $node->get_first_child_token();
if ( WP_MySQL_Lexer::LIKE_SYMBOL === $token->id ) {
- return $this->translate_like( $ast );
+ return $this->translate_like( $node );
} elseif ( WP_MySQL_Lexer::REGEXP_SYMBOL === $token->id ) {
- return $this->translate_regexp_functions( $ast );
+ return $this->translate_regexp_functions( $node );
}
- return $this->translate_sequence( $ast->get_children() );
+ return $this->translate_sequence( $node->get_children() );
case 'runtimeFunctionCall':
- return $this->translate_runtime_function_call( $ast );
+ return $this->translate_runtime_function_call( $node );
case 'functionCall':
- return $this->translate_function_call( $ast );
+ return $this->translate_function_call( $node );
case 'systemVariable':
// @TODO: Emulate some system variables, or use reasonable defaults.
// See: https://dev.mysql.com/doc/refman/8.4/en/server-system-variable-reference.html
@@ -1493,10 +1612,10 @@ private function translate( $ast ) {
return 'NULL';
case 'castType':
// Translate "CAST(... AS BINARY)" to "CAST(... AS BLOB)".
- if ( $ast->has_child_token( WP_MySQL_Lexer::BINARY_SYMBOL ) ) {
+ if ( $node->has_child_token( WP_MySQL_Lexer::BINARY_SYMBOL ) ) {
return 'BLOB';
}
- return $this->translate_sequence( $ast->get_children() );
+ return $this->translate_sequence( $node->get_children() );
case 'defaultCollation':
// @TODO: Check and save in information schema.
return null;
@@ -1504,16 +1623,22 @@ private function translate( $ast ) {
// @TODO: How to handle IGNORE/REPLACE?
// The "AS" keyword is optional in MySQL, but required in SQLite.
- return 'AS ' . $this->translate( $ast->get_first_child_node() );
+ return 'AS ' . $this->translate( $node->get_first_child_node() );
case 'indexHint':
case 'indexHintList':
return null;
default:
- return $this->translate_sequence( $ast->get_children() );
+ return $this->translate_sequence( $node->get_children() );
}
}
- private function translate_token( WP_MySQL_Token $token ) {
+ /**
+ * Translate a MySQL token to SQLite.
+ *
+ * @param WP_MySQL_Token $token The MySQL token to translate.
+ * @return string|null The translated value.
+ */
+ private function translate_token( WP_MySQL_Token $token ): ?string {
switch ( $token->id ) {
case WP_MySQL_Lexer::EOF:
return null;
@@ -1521,9 +1646,9 @@ private function translate_token( WP_MySQL_Token $token ) {
return 'AUTOINCREMENT';
case WP_MySQL_Lexer::BINARY_SYMBOL:
/*
- * There is no "BINARY expr" equivalent in SQLite. We can look for
- * the BINARY keyword in particular cases (with REGEXP, LIKE, etc.)
- * and then remove it from the translated output here.
+ * There is no "BINARY expr" equivalent in SQLite. We look for the
+ * keyword from a higher level to respect it in particular cases
+ * (REGEXP, LIKE, etc.) and then remove it from the output here.
*/
return null;
case WP_MySQL_Lexer::SQL_CALC_FOUND_ROWS_SYMBOL:
@@ -1537,6 +1662,14 @@ private function translate_token( WP_MySQL_Token $token ) {
}
}
+ /**
+ * Translate a sequence of MySQL AST nodes to SQLite.
+ *
+ * @param array $nodes The MySQL token to translate.
+ * @param string $separator The separator to use between fragments.
+ * @return string|null The translated value.
+ * @throws WP_SQLite_Driver_Exception When the translation fails.
+ */
private function translate_sequence( array $nodes, string $separator = ' ' ): ?string {
$parts = array();
foreach ( $nodes as $node ) {
@@ -1556,6 +1689,12 @@ private function translate_sequence( array $nodes, string $separator = ' ' ): ?s
return implode( $separator, $parts );
}
+ /**
+ * Translate a MySQL string literal to SQLite.
+ *
+ * @param WP_Parser_Node $node The "textStringLiteral" AST node.
+ * @return string The translated value.
+ */
private function translate_string_literal( WP_Parser_Node $node ): string {
$token = $node->get_first_child_token();
@@ -1639,6 +1778,12 @@ private function translate_string_literal( WP_Parser_Node $node ): string {
return $parts[0];
}
+ /**
+ * Translate a MySQL pure identifier to SQLite.
+ *
+ * @param WP_Parser_Node $node The "pureIdentifier" AST node.
+ * @return string The translated value.
+ */
private function translate_pure_identifier( WP_Parser_Node $node ): string {
$token = $node->get_first_child_token();
@@ -1655,6 +1800,13 @@ private function translate_pure_identifier( WP_Parser_Node $node ): string {
return '`' . str_replace( '`', '``', $value ) . '`';
}
+ /**
+ * Translate a MySQL simple expression to SQLite.
+ *
+ * @param WP_Parser_Node $node The "simpleExpr" AST node.
+ * @return string The translated value.
+ * @throws WP_SQLite_Driver_Exception When the translation fails.
+ */
private function translate_simple_expr( WP_Parser_Node $node ): string {
$token = $node->get_first_child_token();
@@ -1669,6 +1821,13 @@ private function translate_simple_expr( WP_Parser_Node $node ): string {
return $this->translate_sequence( $node->get_children() );
}
+ /**
+ * Translate a MySQL LIKE expression to SQLite.
+ *
+ * @param WP_Parser_Node $node The "predicateOperations" AST node.
+ * @return string The translated value.
+ * @throws WP_SQLite_Driver_Exception When the translation fails.
+ */
private function translate_like( WP_Parser_Node $node ): string {
$tokens = $node->get_descendant_tokens();
$is_binary = isset( $tokens[1] ) && WP_MySQL_Lexer::BINARY_SYMBOL === $tokens[1]->id;
@@ -1702,6 +1861,13 @@ private function translate_like( WP_Parser_Node $node ): string {
return $this->translate_sequence( $node->get_children() ) . " ESCAPE '\\'";
}
+ /**
+ * Translate MySQL REGEXP expression to SQLite.
+ *
+ * @param WP_Parser_Node $node The "predicateOperations" AST node.
+ * @return string The translated value.
+ * @throws WP_SQLite_Driver_Exception When the translation fails.
+ */
private function translate_regexp_functions( WP_Parser_Node $node ): string {
$tokens = $node->get_descendant_tokens();
$is_binary = isset( $tokens[1] ) && WP_MySQL_Lexer::BINARY_SYMBOL === $tokens[1]->id;
@@ -1725,6 +1891,13 @@ private function translate_regexp_functions( WP_Parser_Node $node ): string {
return 'REGEXP ' . $this->translate( $node->get_first_child_node() );
}
+ /**
+ * Translate a MySQL runtime function call to SQLite.
+ *
+ * @param WP_Parser_Node $node The "runtimeFunctionCall" AST node.
+ * @return string The translated value.
+ * @throws WP_SQLite_Driver_Exception When the translation fails.
+ */
private function translate_runtime_function_call( WP_Parser_Node $node ): string {
$child = $node->get_first_child();
if ( $child instanceof WP_Parser_Node ) {
@@ -1768,6 +1941,13 @@ private function translate_runtime_function_call( WP_Parser_Node $node ): string
}
}
+ /**
+ * Translate a MySQL function call to SQLite.
+ *
+ * @param WP_Parser_Node $node The "functionCall" AST node.
+ * @return string The translated value.
+ * @throws WP_SQLite_Driver_Exception When the translation fails.
+ */
private function translate_function_call( WP_Parser_Node $node ): string {
$nodes = $node->get_child_nodes();
$name = strtoupper(
@@ -1843,6 +2023,12 @@ private function translate_function_call( WP_Parser_Node $node ): string {
}
}
+ /**
+ * Translate a MySQL datetime literal to SQLite.
+ *
+ * @param string $value The MySQL datetime literal.
+ * @return string The translated value.
+ */
private function translate_datetime_literal( string $value ): string {
/*
* The code below converts the date format to one preferred by SQLite.
@@ -1902,6 +2088,42 @@ private function translate_datetime_literal( string $value ): string {
return $value;
}
+ /**
+ * Translate a MySQL SHOW LIKE ... or SHOW WHERE ... condition to SQLite.
+ *
+ * @param WP_Parser_Node $like_or_where The "likeOrWhere" AST node.
+ * @return string The translated value.
+ * @throws WP_SQLite_Driver_Exception When the translation fails.
+ */
+ private function translate_show_like_or_where_condition( WP_Parser_Node $like_or_where ): string {
+ $like_clause = $like_or_where->get_first_child_node( 'likeClause' );
+ if ( null !== $like_clause ) {
+ $value = $this->translate(
+ $like_clause->get_first_child_node( 'textStringLiteral' )
+ );
+ return sprintf( "AND table_name LIKE %s ESCAPE '\\'", $value );
+ }
+
+ $where_clause = $like_or_where->get_first_child_node( 'whereClause' );
+ if ( null !== $where_clause ) {
+ $value = $this->translate(
+ $where_clause->get_first_child_node( 'expr' )
+ );
+ return sprintf( 'AND %s', $value );
+ }
+
+ return '';
+ }
+
+
+ /**
+ * Generate a SQLite CREATE TABLE statement from information schema data.
+ *
+ * @param string $table_name The name of the table to create.
+ * @param string|null $new_table_name Override the original table name for ALTER TABLE emulation.
+ * @return string[] Queries to create the table, indexes, and constraints.
+ * @throws WP_SQLite_Driver_Exception When the table information is missing.
+ */
private function get_sqlite_create_table_statement( string $table_name, ?string $new_table_name = null ): array {
// 1. Get table info.
$table_info = $this->execute_sqlite_query(
@@ -2075,6 +2297,12 @@ function ( $column ) {
return array_merge( array( $create_table_query ), $create_index_queries, $on_update_queries );
}
+ /**
+ * Generate a MySQL CREATE TABLE statement from information schema data.
+ *
+ * @param string $table_name The name of the table to create.
+ * @return string The CREATE TABLE statement.
+ */
private function get_mysql_create_table_statement( string $table_name ): ?string {
// 1. Get table info.
$table_info = $this->execute_sqlite_query(
@@ -2200,26 +2428,16 @@ function ( $column ) {
return $sql;
}
- private function get_show_like_or_where_condition( WP_Parser_Node $like_or_where ): string {
- $like_clause = $like_or_where->get_first_child_node( 'likeClause' );
- if ( null !== $like_clause ) {
- $value = $this->translate(
- $like_clause->get_first_child_node( 'textStringLiteral' )
- );
- return sprintf( "AND table_name LIKE %s ESCAPE '\\'", $value );
- }
-
- $where_clause = $like_or_where->get_first_child_node( 'whereClause' );
- if ( null !== $where_clause ) {
- $value = $this->translate(
- $where_clause->get_first_child_node( 'expr' )
- );
- return sprintf( 'AND %s', $value );
- }
-
- return '';
- }
+ /**
+ * Get an SQLite query to emulate MySQL "ON UPDATE CURRENT_TIMESTAMP".
+ *
+ * In SQLite, "ON UPDATE CURRENT_TIMESTAMP" is not supported. We need to
+ * create a trigger to emulate this behavior.
+ *
+ * @param string $table The table name.
+ * @param string $column The column name.
+ */
private function get_column_on_update_trigger_query( string $table, string $column ): string {
// The trigger wouldn't work for virtual and "WITHOUT ROWID" tables,
// but currently that can't happen as we're not creating such tables.
@@ -2235,6 +2453,14 @@ private function get_column_on_update_trigger_query( string $table, string $colu
";
}
+ /**
+ * Unquote a quoted SQLite identifier.
+ *
+ * Remove bounding quotes and replace escaped quotes with their values.
+ *
+ * @param string $quoted_identifier The quoted identifier value.
+ * @return string The unquoted identifier value.
+ */
private function unquote_sqlite_identifier( string $quoted_identifier ): string {
$first_byte = $quoted_identifier[0] ?? null;
if ( '"' === $first_byte || '`' === $first_byte ) {
@@ -2245,18 +2471,35 @@ private function unquote_sqlite_identifier( string $quoted_identifier ): string
return str_replace( $first_byte . $first_byte, $first_byte, $unquoted );
}
+ /**
+ * Quote an SQLite identifier.
+ *
+ * Wrap the identifier in backticks and escape backtick values within.
+ *
+ * @param string $unquoted_identifier The unquoted identifier value.
+ * @return string The quoted identifier value.
+ */
private function quote_sqlite_identifier( string $unquoted_identifier ): string {
return '`' . str_replace( '`', '``', $unquoted_identifier ) . '`';
}
+
+ /**
+ * Quote a MySQL identifier.
+ *
+ * Wrap the identifier in backticks and escape backtick values within.
+ *
+ * @param string $unquoted_identifier The unquoted identifier value.
+ * @return string The quoted identifier value.
+ */
private function quote_mysql_identifier( string $unquoted_identifier ): string {
return '`' . str_replace( '`', '``', $unquoted_identifier ) . '`';
}
/**
- * Method to clear previous data.
+ * Clear the state of the driver.
*/
- private function flush() {
+ private function flush(): void {
$this->last_mysql_query = '';
$this->last_sqlite_queries = array();
$this->last_result = null;
@@ -2264,29 +2507,28 @@ private function flush() {
}
/**
- * Method to set the results from the fetched data.
+ * Set results of a query() call using fetched data.
*
* @param array $data The data to set.
*/
- private function set_results_from_fetched_data( $data ) {
+ private function set_results_from_fetched_data( array $data ): void {
$this->last_result = $data;
$this->last_return_value = $this->last_result;
}
/**
- * Method to set the results from the affected rows.
+ * Set results of a query() call using the number of affected rows.
*
* @param int|null $override Override the affected rows.
*/
- private function set_result_from_affected_rows( $override = null ) {
+ private function set_result_from_affected_rows( int $override = null ): void {
/*
- * SELECT CHANGES() is a workaround for the fact that
- * $stmt->rowCount() returns "0" (zero) with the
- * SQLite driver at all times.
- * Source: https://www.php.net/manual/en/pdostatement.rowcount.php
+ * SELECT CHANGES() is a workaround for the fact that $stmt->rowCount()
+ * returns "0" (zero) with the SQLite driver at all times.
+ * See: https://www.php.net/manual/en/pdostatement.rowcount.php
*/
if ( null === $override ) {
- $affected_rows = (int) $this->execute_sqlite_query( 'select changes()' )->fetch()[0];
+ $affected_rows = (int) $this->execute_sqlite_query( 'SELECT CHANGES()' )->fetch()[0];
} else {
$affected_rows = $override;
}
@@ -2294,6 +2536,14 @@ private function set_result_from_affected_rows( $override = null ) {
$this->last_return_value = $affected_rows;
}
+ /**
+ * Create a new SQLite driver exception.
+ *
+ * @param string $message The exception message.
+ * @param int $code The exception code.
+ * @param Throwable|null $previous The previous exception.
+ * @return WP_SQLite_Driver_Exception
+ */
private function new_driver_exception(
string $message,
int $code = 0,
@@ -2302,10 +2552,26 @@ private function new_driver_exception(
return new WP_SQLite_Driver_Exception( $this, $message, $code, $previous );
}
+ /**
+ * Create a new invalid input exception.
+ *
+ * This exception can be used to mark cases that should never occur according
+ * to the MySQL grammar. It may serve as an assertion that should never fail.
+ *
+ * @return WP_SQLite_Driver_Exception
+ */
private function new_invalid_input_exception(): WP_SQLite_Driver_Exception {
- throw new WP_SQLite_Driver_Exception( $this, 'MySQL query syntax error.' );
+ return new WP_SQLite_Driver_Exception( $this, 'MySQL query syntax error.' );
}
+ /**
+ * Create a new not supported exception.
+ *
+ * This exception can be used to mark MySQL constructs that are not supported.
+ *
+ * @param string $cause The cause, indicating which construct is not supported.
+ * @return WP_SQLite_Driver_Exception
+ */
private function new_not_supported_exception( string $cause ): WP_SQLite_Driver_Exception {
return new WP_SQLite_Driver_Exception(
$this,
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
index a0e13e1..ac4322a 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php
@@ -1,5 +1,12 @@
db_name = $db_name;
+ public function __construct( string $database, callable $query_callback ) {
+ $this->db_name = $database;
$this->query_callback = $query_callback;
}
/**
- * Ensure that the supported information schema tables exist in the SQLite
+ * Ensure that the information schema tables exist in the SQLite
* database. Tables that are missing will be created.
*/
public function ensure_information_schema_tables(): void {
@@ -356,7 +363,7 @@ public function ensure_information_schema_tables(): void {
/**
* Analyze CREATE TABLE statement and record data in the information schema.
*
- * @param WP_Parser_Node $node AST node representing a CREATE TABLE statement.
+ * @param WP_Parser_Node $node The "createStatement" AST node with "createTable" child.
*/
public function record_create_table( WP_Parser_Node $node ): void {
$table_name = $this->get_value( $node->get_first_descendant_node( 'tableName' ) );
@@ -414,6 +421,11 @@ public function record_create_table( WP_Parser_Node $node ): void {
}
}
+ /**
+ * Analyze ALTER TABLE statement and record data in the information schema.
+ *
+ * @param WP_Parser_Node $node The "alterStatement" AST node with "alterTable" child.
+ */
public function record_alter_table( WP_Parser_Node $node ): void {
$table_name = $this->get_value( $node->get_first_descendant_node( 'tableRef' ) );
$actions = $node->get_descendant_nodes( 'alterListItem' );
@@ -496,6 +508,11 @@ public function record_alter_table( WP_Parser_Node $node ): void {
}
}
+ /**
+ * Analyze DROP TABLE statement and record data in the information schema.
+ *
+ * @param WP_Parser_Node $node The "dropStatement" AST node with "dropTable" child.
+ */
public function record_drop_table( WP_Parser_Node $node ): void {
$child_node = $node->get_first_child_node();
if ( $child_node->has_child_token( WP_MySQL_Lexer::TEMPORARY_SYMBOL ) ) {
@@ -531,6 +548,13 @@ public function record_drop_table( WP_Parser_Node $node ): void {
// @TODO: RESTRICT vs. CASCADE
}
+ /**
+ * Analyze ADD COLUMN definition and record data in the information schema.
+ *
+ * @param string $table_name The table name.
+ * @param string $column_name The column name.
+ * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
+ */
private function record_add_column( string $table_name, string $column_name, WP_Parser_Node $node ): void {
$position = $this->query(
'
@@ -551,6 +575,14 @@ private function record_add_column( string $table_name, string $column_name, WP_
}
}
+ /**
+ * Analyze CHANGE COLUMN definition and record data in the information schema.
+ *
+ * @param string $table_name The table name.
+ * @param string $column_name The column name.
+ * @param string $new_column_name The new column name when the column is renamed.
+ * @param WP_Parser_Node $node The "fieldDefinition" AST node.
+ */
private function record_change_column(
string $table_name,
string $column_name,
@@ -597,6 +629,13 @@ private function record_change_column(
}
}
+ /**
+ * Analyze MODIFY COLUMN definition and record data in the information schema.
+ *
+ * @param string $table_name The table name.
+ * @param string $column_name The column name.
+ * @param WP_Parser_Node $node The "fieldDefinition" AST node.
+ */
private function record_modify_column(
string $table_name,
string $column_name,
@@ -605,6 +644,12 @@ private function record_modify_column(
$this->record_change_column( $table_name, $column_name, $column_name, $node );
}
+ /**
+ * Record DROP COLUMN data in the information schema.
+ *
+ * @param string $table_name The table name.
+ * @param string $column_name The column name.
+ */
private function record_drop_column( $table_name, $column_name ): void {
$this->delete_values(
'_mysql_information_schema_columns',
@@ -642,6 +687,12 @@ private function record_drop_column( $table_name, $column_name ): void {
$this->sync_column_key_info( $table_name );
}
+ /**
+ * Record DROP INDEX data in the information schema.
+ *
+ * @param string $table_name The table name.
+ * @param string $index_name The index name.
+ */
private function record_drop_index( string $table_name, string $index_name ): void {
$this->delete_values(
'_mysql_information_schema_statistics',
@@ -654,6 +705,12 @@ private function record_drop_index( string $table_name, string $index_name ): vo
$this->sync_column_key_info( $table_name );
}
+ /**
+ * Analyze ADD CONSTRAINT definition and record data in the information schema.
+ *
+ * @param string $table_name The table name.
+ * @param WP_Parser_Node $node The "tableConstraintDef" AST node.
+ */
private function record_add_constraint( string $table_name, WP_Parser_Node $node ): void {
// Get first constraint keyword.
$children = $node->get_children();
@@ -767,6 +824,15 @@ private function record_add_constraint( string $table_name, WP_Parser_Node $node
$this->sync_column_key_info( $table_name );
}
+ /**
+ * Analyze "columnDefinition" or "fieldDefinition" AST node and extract column data.
+ *
+ * @param string $table_name The table name.
+ * @param string $column_name The column name.
+ * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
+ * @param int $position The ordinal position of the column in the table.
+ * @return array Column data for the information schema.
+ */
private function extract_column_data( string $table_name, string $column_name, WP_Parser_Node $node, int $position ): array {
$default = $this->get_column_default( $node );
$nullable = $this->get_column_nullable( $node );
@@ -806,6 +872,15 @@ private function extract_column_data( string $table_name, string $column_name, W
);
}
+ /**
+ * Analyze "columnDefinition" or "fieldDefinition" AST node and extract constraint data.
+ *
+ * @param string $table_name The table name.
+ * @param string $column_name The column name.
+ * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
+ * @param bool $nullable Whether the column is nullable.
+ * @return array|null Constraint data for the information schema.
+ */
private function extract_column_constraint_data( string $table_name, string $column_name, WP_Parser_Node $node, bool $nullable ): ?array {
// Handle inline PRIMARY KEY and UNIQUE constraints.
$has_inline_primary_key = null !== $node->get_first_descendant_token( WP_MySQL_Lexer::KEY_SYMBOL );
@@ -880,6 +955,12 @@ private function sync_column_key_info( string $table_name ): void {
);
}
+ /**
+ * Extract table engine value from the "createStatement" AST node.
+ *
+ * @param WP_Parser_Node $node The "createStatement" AST node with "createTable" child.
+ * @return string The table engine as stored in information schema.
+ */
private function get_table_engine( WP_Parser_Node $node ): string {
$engine_node = $node->get_first_descendant_node( 'engineRef' );
if ( null === $engine_node ) {
@@ -895,6 +976,12 @@ private function get_table_engine( WP_Parser_Node $node ): string {
return $engine;
}
+ /**
+ * Extract table collation value from the "createStatement" AST node.
+ *
+ * @param WP_Parser_Node $node The "createStatement" AST node with "createTable" child.
+ * @return string The table collation as stored in information schema.
+ */
private function get_table_collation( WP_Parser_Node $node ): string {
$collate_node = $node->get_first_descendant_node( 'collationName' );
if ( null === $collate_node ) {
@@ -904,6 +991,12 @@ private function get_table_collation( WP_Parser_Node $node ): string {
return strtolower( $this->get_value( $collate_node ) );
}
+ /**
+ * Extract column default value from the "columnDefinition" or "fieldDefinition" AST node.
+ *
+ * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
+ * @return string The column default as stored in information schema.
+ */
private function get_column_default( WP_Parser_Node $node ): ?string {
$default_attr = null;
foreach ( $node->get_descendant_nodes( 'columnAttribute' ) as $attr ) {
@@ -932,6 +1025,12 @@ private function get_column_default( WP_Parser_Node $node ): ?string {
return $this->get_value( $default_attr->get_first_child_node() );
}
+ /**
+ * Extract column nullability from the "columnDefinition" or "fieldDefinition" AST node.
+ *
+ * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
+ * @return string The column nullability as stored in information schema.
+ */
private function get_column_nullable( WP_Parser_Node $node ): string {
// SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
$data_type = $node->get_first_descendant_node( 'dataType' );
@@ -956,33 +1055,45 @@ private function get_column_nullable( WP_Parser_Node $node ): string {
return 'YES';
}
- private function get_column_key( WP_Parser_Node $column_node ): string {
+ /**
+ * Extract column key info from the "columnDefinition" or "fieldDefinition" AST node.
+ *
+ * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
+ * @return string The column key info as stored in information schema.
+ */
+ private function get_column_key( WP_Parser_Node $node ): string {
// 1. PRI: Column is a primary key or its any component.
if (
- null !== $column_node->get_first_descendant_token( WP_MySQL_Lexer::KEY_SYMBOL )
+ null !== $node->get_first_descendant_token( WP_MySQL_Lexer::KEY_SYMBOL )
) {
return 'PRI';
}
// SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
- $data_type = $column_node->get_first_descendant_node( 'dataType' );
+ $data_type = $node->get_first_descendant_node( 'dataType' );
if ( null !== $data_type->get_first_descendant_token( WP_MySQL_Lexer::SERIAL_SYMBOL ) ) {
return 'PRI';
}
// 2. UNI: Column has UNIQUE constraint.
- if ( null !== $column_node->get_first_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ) ) {
+ if ( null !== $node->get_first_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ) ) {
return 'UNI';
}
// 3. MUL: Column has INDEX.
- if ( null !== $column_node->get_first_descendant_token( WP_MySQL_Lexer::INDEX_SYMBOL ) ) {
+ if ( null !== $node->get_first_descendant_token( WP_MySQL_Lexer::INDEX_SYMBOL ) ) {
return 'MUL';
}
return '';
}
+ /**
+ * Extract column extra from the "columnDefinition" or "fieldDefinition" AST node.
+ *
+ * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
+ * @return string The column extra as stored in information schema.
+ */
private function get_column_extra( WP_Parser_Node $node ): string {
$extras = array();
$attributes = $node->get_descendant_nodes( 'columnAttribute' );
@@ -1023,6 +1134,12 @@ private function get_column_extra( WP_Parser_Node $node ): string {
return implode( ' ', $extras );
}
+ /**
+ * Extract column comment from the "columnDefinition" or "fieldDefinition" AST node.
+ *
+ * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
+ * @return string The column comment as stored in information schema.
+ */
private function get_column_comment( WP_Parser_Node $node ): string {
foreach ( $node->get_descendant_nodes( 'columnAttribute' ) as $attr ) {
if ( $attr->has_child_token( WP_MySQL_Lexer::COMMENT_SYMBOL ) ) {
@@ -1032,6 +1149,12 @@ private function get_column_comment( WP_Parser_Node $node ): string {
return '';
}
+ /**
+ * Extract column data type from the "columnDefinition" or "fieldDefinition" AST node.
+ *
+ * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
+ * @return array{ string, string } The data type and column type as stored in information schema.
+ */
private function get_column_data_types( WP_Parser_Node $node ): array {
$type_node = $node->get_first_descendant_node( 'dataType' );
$type = $type_node->get_descendant_tokens();
@@ -1150,6 +1273,13 @@ private function get_column_data_types( WP_Parser_Node $node ): array {
return array( $type, $full_type );
}
+ /**
+ * Extract column charset and collation from the "columnDefinition" or "fieldDefinition" AST node.
+ *
+ * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
+ * @param string $data_type The column data type as stored in information schema.
+ * @return array{ string|null, string|null } The column charset and collation as stored in information schema.
+ */
private function get_column_charset_and_collation( WP_Parser_Node $node, string $data_type ): array {
if ( ! (
'char' === $data_type
@@ -1235,6 +1365,14 @@ private function get_column_charset_and_collation( WP_Parser_Node $node, string
return array( $charset, $collation );
}
+ /**
+ * Extract column length info from the "columnDefinition" or "fieldDefinition" AST node.
+ *
+ * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
+ * @param string $data_type The column data type as stored in information schema.
+ * @param string|null $charset The column charset as stored in information schema.
+ * @return array{ int|null, int|null } The column char length and octet length as stored in information schema.
+ */
private function get_column_lengths( WP_Parser_Node $node, string $data_type, ?string $charset ): array {
// Text and blob types.
if ( 'tinytext' === $data_type || 'tinyblob' === $data_type ) {
@@ -1284,6 +1422,13 @@ private function get_column_lengths( WP_Parser_Node $node, string $data_type, ?s
return array( null, null );
}
+ /**
+ * Extract column precision and scale from the "columnDefinition" or "fieldDefinition" AST node.
+ *
+ * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
+ * @param string $data_type The column data type as stored in information schema.
+ * @return array{ int|null, int|null } The column precision and scale as stored in information schema.
+ */
private function get_column_numeric_attributes( WP_Parser_Node $node, string $data_type ): array {
if ( 'tinyint' === $data_type ) {
return array( 3, 0 );
@@ -1344,6 +1489,13 @@ private function get_column_numeric_attributes( WP_Parser_Node $node, string $da
return array( null, null );
}
+ /**
+ * Extract column date/time precision from the "columnDefinition" or "fieldDefinition" AST node.
+ *
+ * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
+ * @param string $data_type The column data type as stored in information schema.
+ * @return int|null The date/time precision as stored in information schema.
+ */
private function get_column_datetime_precision( WP_Parser_Node $node, string $data_type ): ?int {
if ( 'time' === $data_type || 'datetime' === $data_type || 'timestamp' === $data_type ) {
$precision = $node->get_first_descendant_node( 'typeDatetimePrecision' );
@@ -1356,6 +1508,12 @@ private function get_column_datetime_precision( WP_Parser_Node $node, string $da
return null;
}
+ /**
+ * Extract column generation expression from the "columnDefinition" or "fieldDefinition" AST node.
+ *
+ * @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
+ * @return string The column generation expression as stored in information schema.
+ */
private function get_column_generation_expression( WP_Parser_Node $node ): string {
if ( null !== $node->get_first_descendant_token( WP_MySQL_Lexer::GENERATED_SYMBOL ) ) {
$expr = $node->get_first_descendant_node( 'exprWithParentheses' );
@@ -1364,6 +1522,12 @@ private function get_column_generation_expression( WP_Parser_Node $node ): strin
return '';
}
+ /**
+ * Extract index name from the "tableConstraintDef" AST node.
+ *
+ * @param WP_Parser_Node $node The "tableConstraintDef" AST node.
+ * @return string The index name as stored in information schema.
+ */
private function get_index_name( WP_Parser_Node $node ): string {
if ( $node->get_first_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) {
return 'PRIMARY';
@@ -1389,6 +1553,12 @@ private function get_index_name( WP_Parser_Node $node ): string {
return $this->get_value( $name_node );
}
+ /**
+ * Extract index non-unique value from the "tableConstraintDef" AST node.
+ *
+ * @param WP_MySQL_Token $token The first constraint keyword.
+ * @return int The value of non-unique as stored in information schema.
+ */
private function get_index_non_unique( WP_MySQL_Token $token ): int {
if (
WP_MySQL_Lexer::PRIMARY_SYMBOL === $token->id
@@ -1399,6 +1569,14 @@ private function get_index_non_unique( WP_MySQL_Token $token ): int {
return 1;
}
+ /**
+ * Extract index type from the "tableConstraintDef" AST node.
+ *
+ * @param WP_Parser_Node $node The "tableConstraintDef" AST node.
+ * @param WP_MySQL_Token $token The first constraint keyword.
+ * @param bool $has_spatial_column Whether the index contains a spatial column.
+ * @return string The index type as stored in information schema.
+ */
private function get_index_type(
WP_Parser_Node $node,
WP_MySQL_Token $token,
@@ -1434,6 +1612,12 @@ private function get_index_type(
return 'BTREE';
}
+ /**
+ * Extract index column name from the "keyPart" AST node.
+ *
+ * @param WP_Parser_Node $node The "keyPart" AST node.
+ * @return string The index column name as stored in information schema.
+ */
private function get_index_column_name( WP_Parser_Node $node ): ?string {
if ( 'keyPart' !== $node->rule_name ) {
return null;
@@ -1441,6 +1625,13 @@ private function get_index_column_name( WP_Parser_Node $node ): ?string {
return $this->get_value( $node->get_first_descendant_node( 'identifier' ) );
}
+ /**
+ * Extract index column name from the "keyPart" AST node.
+ *
+ * @param WP_Parser_Node $node The "keyPart" AST node.
+ * @param string $index_type The index type as stored in information schema.
+ * @return string The index column name as stored in information schema.
+ */
private function get_index_column_collation( WP_Parser_Node $node, string $index_type ): ?string {
if ( 'FULLTEXT' === $index_type ) {
return null;
@@ -1454,6 +1645,14 @@ private function get_index_column_collation( WP_Parser_Node $node, string $index
return 'DESC' === $collate ? 'D' : 'A';
}
+ /**
+ * Extract index column sub-part value from the "keyPart" AST node.
+ *
+ * @param WP_Parser_Node $node The "keyPart" AST node.
+ * @param int|null $max_length The maximum character length of the index column.
+ * @param bool $is_spatial Whether the index column is a spatial column.
+ * @return int|null The index column sub-part value as stored in information schema.
+ */
private function get_index_column_sub_part(
WP_Parser_Node $node,
?int $max_length,
@@ -1474,6 +1673,12 @@ private function get_index_column_sub_part(
return $value;
}
+ /**
+ * Determine whether the column data type is a spatial data type.
+ *
+ * @param string $data_type The column data type as stored in information schema.
+ * @return bool Whether the column data type is a spatial data type.
+ */
private function is_spatial_data_type( string $data_type ): bool {
return 'geometry' === $data_type
|| 'geomcollection' === $data_type
@@ -1499,8 +1704,8 @@ private function is_spatial_data_type( string $data_type ): bool {
* serialize the whole node, in the case of expressions. This may mean
* implementing an MySQL AST -> string printer.
*
- * @param WP_Parser_Node $node
- * @return string
+ * @param WP_Parser_Node $node The AST node that needs to be serialized.
+ * @return string The serialized value of the node.
*/
private function get_value( WP_Parser_Node $node ): string {
$full_value = '';
@@ -1528,6 +1733,12 @@ private function get_value( WP_Parser_Node $node ): string {
return $full_value;
}
+ /**
+ * Insert values into an SQLite table.
+ *
+ * @param string $table_name The name of the table.
+ * @param array $data The data to insert (key is column name, value is column value).
+ */
private function insert_values( string $table_name, array $data ): void {
$this->query(
'
@@ -1538,6 +1749,13 @@ private function insert_values( string $table_name, array $data ): void {
);
}
+ /**
+ * Update values in an SQLite table.
+ *
+ * @param string $table_name The name of the table.
+ * @param array $data The data to update (key is column name, value is column value).
+ * @param array $where The WHERE clause conditions (key is column name, value is column value).
+ */
private function update_values( string $table_name, array $data, array $where ): void {
$set = array();
foreach ( $data as $column => $value ) {
@@ -1559,6 +1777,12 @@ private function update_values( string $table_name, array $data, array $where ):
);
}
+ /**
+ * Delete values from an SQLite table.
+ *
+ * @param string $table_name The name of the table.
+ * @param array $where The WHERE clause conditions (key is column name, value is column value).
+ */
private function delete_values( string $table_name, array $where ): void {
$where_clause = array();
foreach ( $where as $column => $value ) {
@@ -1575,8 +1799,11 @@ private function delete_values( string $table_name, array $where ): void {
}
/**
- * @param string $query
- * @param array $params
+ * Execute an SQLite query.
+ *
+ * @param string $query The query to execute.
+ * @param array $params The query parameters.
+ *
* @return PDOStatement
*/
private function query( string $query, array $params = array() ) {
From 4d9d43269e87d75ba77608183d1b80ea53abb319 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Fri, 7 Feb 2025 16:39:07 +0100
Subject: [PATCH 121/124] Check for mimimum SQLite version
---
.../sqlite-ast/class-wp-sqlite-driver.php | 26 ++++++++++++++++---
1 file changed, 23 insertions(+), 3 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index f58339e..15eb782 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -20,6 +20,14 @@ class WP_SQLite_Driver {
*/
const MYSQL_GRAMMAR_PATH = __DIR__ . '/../../wp-includes/mysql/mysql-grammar.php';
+ /**
+ * The minimum required version of SQLite.
+ *
+ * Currently, we require SQLite >= 3.37.0 due to the STRICT table support:
+ * https://www.sqlite.org/stricttables.html
+ */
+ const MINIMUM_SQLITE_VERSION = '3.37.0';
+
/**
* The default timeout in seconds for SQLite to wait for a writable lock.
*/
@@ -365,6 +373,21 @@ public function __construct( array $options ) {
// Return all values (except null) as strings.
$this->pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
+ // Check the SQLite version.
+ $sqlite_version = $this->get_sqlite_version();
+ if ( version_compare( $sqlite_version, self::MINIMUM_SQLITE_VERSION, '<' ) ) {
+ throw $this->new_driver_exception(
+ sprintf(
+ 'The SQLite version %s is not supported. Minimum required version is %s.',
+ $sqlite_version,
+ self::MINIMUM_SQLITE_VERSION
+ )
+ );
+ }
+
+ // Load SQLite version to a property used by WordPress health info.
+ $this->client_info = $sqlite_version;
+
// Enable foreign keys. By default, they are off.
$this->pdo->query( 'PRAGMA foreign_keys = ON' );
@@ -394,9 +417,6 @@ public function __construct( array $options ) {
array( $this, 'execute_sqlite_query' )
);
$this->information_schema_builder->ensure_information_schema_tables();
-
- // Load SQLite version to a property used by WordPress health info.
- $this->client_info = $this->get_sqlite_version();
}
/**
From 078e2ced079cb1973697cc5b3e3476b658223237 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 29 Jan 2025 15:22:23 +0100
Subject: [PATCH 122/124] Remove the AST driver files from .gitattributes
---
.gitattributes | 3 ---
1 file changed, 3 deletions(-)
diff --git a/.gitattributes b/.gitattributes
index bdd976d..6a7e147 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -8,7 +8,4 @@ phpunit.xml.dist export-ignore
/.github export-ignore
/grammar-tools export-ignore
/tests export-ignore
-/wp-includes/mysql export-ignore
-/wp-includes/parser export-ignore
-/wp-includes/sqlite-ast export-ignore
/wp-includes/sqlite/class-wp-sqlite-crosscheck-db.php export-ignore
From 694701994d780cda35e5499bf8696f3a3a354e3e Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 12 Feb 2025 08:53:24 +0100
Subject: [PATCH 123/124] Improve constant name
---
wp-includes/sqlite-ast/class-wp-sqlite-driver.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index 15eb782..e97e388 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -182,7 +182,7 @@ class WP_SQLite_Driver {
* https://www.sqlite.org/lang_datefunc.html
* https://strftime.org/
*/
- const DATE_FORMAT_TO_STRFTIME_MAP = array(
+ const MYSQL_DATE_FORMAT_TO_SQLITE_STRFTIME_MAP = array(
'%a' => '%D',
'%b' => '%M',
'%c' => '%n',
@@ -1985,7 +1985,7 @@ private function translate_function_call( WP_Parser_Node $node ): string {
case 'DATE_FORMAT':
list ( $date, $mysql_format ) = $args;
- $format = strtr( $mysql_format, self::DATE_FORMAT_TO_STRFTIME_MAP );
+ $format = strtr( $mysql_format, self::MYSQL_DATE_FORMAT_TO_SQLITE_STRFTIME_MAP );
if ( ! $format ) {
throw $this->new_driver_exception(
sprintf(
From 9c0c9b5b8ddb015c1a8eb4b6a9ea314769552fd7 Mon Sep 17 00:00:00 2001
From: Jan Jakes
Date: Wed, 12 Feb 2025 08:59:13 +0100
Subject: [PATCH 124/124] Use past tense in @CHANGED comments
---
grammar-tools/MySQLParser.g4 | 134 +++++++++++++++++------------------
1 file changed, 67 insertions(+), 67 deletions(-)
diff --git a/grammar-tools/MySQLParser.g4 b/grammar-tools/MySQLParser.g4
index 9279fc5..44669f2 100644
--- a/grammar-tools/MySQLParser.g4
+++ b/grammar-tools/MySQLParser.g4
@@ -153,13 +153,13 @@ alterStatement:
| alterLogfileGroup
| alterServer
// ALTER USER is part of the user management rule.
- | alterInstance /* @CHANGED: Add support for "ALTER INSTANCE ..." statement. */
+ | alterInstance /* @CHANGED: Added support for "ALTER INSTANCE ..." statement. */
)
;
/*
* @CHANGED:
- * Add support for "ALTER INSTANCE ..." statement.
+ * Added support for "ALTER INSTANCE ..." statement.
*/
alterInstance:
{serverVersion >= 50711}? INSTANCE_SYMBOL (
@@ -172,7 +172,7 @@ alterInstance:
;
alterDatabase:
- /* @CHANGED: Make "schemaRef" optional. */
+ /* @CHANGED: Made "schemaRef" optional. */
DATABASE_SYMBOL schemaRef? (
createDatabaseOption+
| {serverVersion < 80000}? UPGRADE_SYMBOL DATA_SYMBOL DIRECTORY_SYMBOL NAME_SYMBOL
@@ -219,7 +219,7 @@ alterTable:
/*
* @CHANGED:
- * Fix "alterTableActions" to solve conflicts between "alterCommandsModifierList" and "alterCommandList".
+ * Fixed "alterTableActions" to solve conflicts between "alterCommandsModifierList" and "alterCommandList".
*/
alterTableActions:
(alterCommandsModifierList COMMA_SYMBOL)? standaloneAlterCommands
@@ -235,7 +235,7 @@ alterTableActions:
/*
* @CHANGED:
- * Fix "alterCommandList" to solve conflicts between "alterCommandsModifierList" prefixes.
+ * Fixed "alterCommandList" to solve conflicts between "alterCommandsModifierList" prefixes.
*/
alterCommandList:
alterCommandsModifierList (COMMA_SYMBOL alterList)?
@@ -320,7 +320,7 @@ alterListItem:
| signedLiteral
)
| DROP_SYMBOL DEFAULT_SYMBOL
- | {serverVersion >= 80023}? SET_SYMBOL visibility /* @CHANGED: Add missing SET VISIBLE/INVISIBLE clause. */
+ | {serverVersion >= 80023}? SET_SYMBOL visibility /* @CHANGED: Added missing SET VISIBLE/INVISIBLE clause. */
)
| {serverVersion >= 80000}? ALTER_SYMBOL INDEX_SYMBOL indexRef visibility
| {serverVersion >= 80017}? ALTER_SYMBOL CHECK_SYMBOL identifier constraintEnforcement
@@ -353,7 +353,7 @@ restrict:
/*
* @CHANGED:
- * Fix ALTER TABLE with ORDER to use 'qualifiedIdentifier' instead of just 'identifier'.
+ * Fixed ALTER TABLE with ORDER to use 'qualifiedIdentifier' instead of just 'identifier'.
* This is necessary to support "t.id" in a query like "ALTER TABLE t ORDER BY t.id".
*/
alterOrderList:
@@ -425,7 +425,7 @@ alterTablespaceOption:
| tsOptionAutoextendSize
| tsOptionMaxSize
| tsOptionEngine
- | {serverVersion >= 80021}? tsOptionEngineAttribute /* @CHANGED: Add missing "ENGINE_ATTRIBUTE" option. */
+ | {serverVersion >= 80021}? tsOptionEngineAttribute /* @CHANGED: Added missing "ENGINE_ATTRIBUTE" option. */
| tsOptionWait
| tsOptionEncryption
;
@@ -496,7 +496,7 @@ createDatabaseOption:
/*
* @CHANGED:
- * Fix "createTable" to solve support "LIKE tableRef" and "LIKE (tableRef)".
+ * Fixed "createTable" to solve support "LIKE tableRef" and "LIKE (tableRef)".
* They need to come before "tableElementList" to avoid misinterpreting "LIKE".
*/
createTable:
@@ -530,7 +530,7 @@ createRoutine: // Rule for external use only.
/*
* @CHANGED:
- * Add missing "ifNotExists?".
+ * Added missing "ifNotExists?".
*/
createProcedure:
definerClause? PROCEDURE_SYMBOL ({serverVersion >= 80029}? ifNotExists?) procedureName OPEN_PAR_SYMBOL (
@@ -540,7 +540,7 @@ createProcedure:
/*
* @CHANGED:
- * Add missing "ifNotExists?".
+ * Added missing "ifNotExists?".
*/
createFunction:
definerClause? FUNCTION_SYMBOL ({serverVersion >= 80029}? ifNotExists?) functionName OPEN_PAR_SYMBOL (
@@ -614,7 +614,7 @@ createIndex:
/*
* @CHANGED:
- * Fix "indexNameAndType" to solve conflicts between "indexName USING_SYMBOL"
+ * Fixed "indexNameAndType" to solve conflicts between "indexName USING_SYMBOL"
* and "indexName TYPE_SYMBOL" prefix by moving them to a single branch.
*/
indexNameAndType:
@@ -694,7 +694,7 @@ tablespaceOption:
| tsOptionExtentSize
| tsOptionNodegroup
| tsOptionEngine
- | {serverVersion >= 80021}? tsOptionEngineAttribute /* @CHANGED: Add missing "ENGINE_ATTRIBUTE" option. */
+ | {serverVersion >= 80021}? tsOptionEngineAttribute /* @CHANGED: Added missing "ENGINE_ATTRIBUTE" option. */
| tsOptionWait
| tsOptionComment
| {serverVersion >= 50707}? tsOptionFileblockSize
@@ -731,7 +731,7 @@ tsOptionEngine:
/*
* @CHANGED:
- * Add missing "ENGINE_ATTRIBUTE" option.
+ * Added missing "ENGINE_ATTRIBUTE" option.
*/
tsOptionEngineAttribute:
ENGINE_ATTRIBUTE_SYMBOL EQUAL_OPERATOR? textStringLiteral
@@ -775,7 +775,7 @@ viewSuid:
/*
* @CHANGED:
- * Add missing "ifNotExists?".
+ * Added missing "ifNotExists?".
*/
createTrigger:
definerClause? TRIGGER_SYMBOL ({serverVersion >= 80029}? ifNotExists?) triggerName timing = (BEFORE_SYMBOL | AFTER_SYMBOL) event = (
@@ -962,7 +962,7 @@ deleteStatementOption: // opt_delete_option in sql_yacc.yy, but the name collide
/*
* @CHANGED:
- * Reorder "selectItemList" and "exprList" to match "selectItemList", as we don't handle versions yet.
+ * Reordered "selectItemList" and "exprList" to match "selectItemList", as we don't handle versions yet.
*/
doStatement:
DO_SYMBOL (
@@ -1100,7 +1100,7 @@ replaceStatement:
/*
* @CHANGED:
- * Fix "selectStatement" to solve conflicts between "queryExpressionParens" and "selectStatementWithInto".
+ * Fixed "selectStatement" to solve conflicts between "queryExpressionParens" and "selectStatementWithInto".
* Since "queryExpression" already contains "queryExpressionParens" as a subrule, we can remove it here.
*/
selectStatement:
@@ -1145,7 +1145,7 @@ selectStatement:
selectStatementWithInto:
OPEN_PAR_SYMBOL selectStatementWithInto CLOSE_PAR_SYMBOL
| queryExpression intoClause lockingClauseList?
- | queryExpression lockingClauseList intoClause /* @CHANGED: Add missing "queryExpression" prefix. */
+ | queryExpression lockingClauseList intoClause /* @CHANGED: Added missing "queryExpression" prefix. */
;
queryExpression:
@@ -1167,7 +1167,7 @@ queryExpression:
/*
* @CHANGED:
- * Implement missing "EXCEPT" and "INTERSECT" operators in the grammar.
+ * Implemented missing "EXCEPT" and "INTERSECT" operators in the grammar.
* Note that "INTERSECT" must have a higher precedence than "UNION" and "EXCEPT",
* and is therefore evaluated first via "queryTerm" as per:
* https://dev.mysql.com/doc/refman/8.0/en/set-operations.html
@@ -1189,7 +1189,7 @@ queryTerm:
/*
* @CHANGED:
- * Rewrite "queryExpressionParens" to keep only "queryExpression" within.
+ * Rewrote "queryExpressionParens" to keep only "queryExpression" within.
* This avoids conflict between "queryExpressionParens" and "queryExpression"
* (which already contains "queryExpressionParens" as a subrule).
*/
@@ -1279,7 +1279,7 @@ windowSpec:
/*
* @CHANGED:
- * Rewrite "windowSpecDetails" so to keep variants with "windowName?" last.
+ * Rewrote "windowSpecDetails" so to keep variants with "windowName?" last.
* We first need to try to match the symbols as keywords, only then as identifiers.
* Identifiers can never take precedence over keywords in the grammar.
*
@@ -1562,7 +1562,7 @@ tableAlias:
/*
* @CHANGED:
- * Fix "indexHintList" to use only whitespace as a separator (not commas).
+ * Fixed "indexHintList" to use only whitespace as a separator (not commas).
*/
indexHintList:
indexHint+
@@ -1618,7 +1618,7 @@ transactionOrLockingStatement:
;
transactionStatement:
- /* @CHANGED: Use "transactionCharacteristicList" instead of "transactionCharacteristic". */
+ /* @CHANGED: Used "transactionCharacteristicList" instead of "transactionCharacteristic". */
START_SYMBOL TRANSACTION_SYMBOL transactionCharacteristicList?
| COMMIT_SYMBOL WORK_SYMBOL? (AND_SYMBOL NO_SYMBOL? CHAIN_SYMBOL)? (
NO_SYMBOL? RELEASE_SYMBOL
@@ -1633,7 +1633,7 @@ beginWork:
/*
* @CHANGED:
- * Add "transactionCharacteristicList" to fix support for transaction with multiple characteristics.
+ * Added "transactionCharacteristicList" to fix support for transaction with multiple characteristics.
*/
transactionCharacteristicList:
transactionCharacteristic (COMMA_SYMBOL transactionCharacteristic)*
@@ -1710,7 +1710,7 @@ xid:
/*
* @CHANGED:
- * Fix "replicationStatement" to correctly support the "RESET PERSIST" statement.
+ * Fixed "replicationStatement" to correctly support the "RESET PERSIST" statement.
* The "ifExists" clause wasn't optional, and "identifier" was used instead of "qualifiedIdentifier".
*/
replicationStatement:
@@ -1814,7 +1814,7 @@ serverIdList:
;
changeReplication:
- /* @CHANGED: Add support for "CHANGE REPLICATION SOURCE ..." statement. */
+ /* @CHANGED: Added support for "CHANGE REPLICATION SOURCE ..." statement. */
CHANGE_SYMBOL REPLICATION_SYMBOL SOURCE_SYMBOL TO_SYMBOL changeReplicationSourceOptions channel?
| CHANGE_SYMBOL REPLICATION_SYMBOL FILTER_SYMBOL filterDefinition (
COMMA_SYMBOL filterDefinition
@@ -1823,7 +1823,7 @@ changeReplication:
/*
* @CHANGED:
- * Add support for "CHANGE REPLICATION SOURCE ..." statement.
+ * Added support for "CHANGE REPLICATION SOURCE ..." statement.
*/
changeReplicationSourceOptions:
replicationSourceOption (COMMA_SYMBOL replicationSourceOption)*
@@ -1831,7 +1831,7 @@ changeReplicationSourceOptions:
/*
* @CHANGED:
- * Add support for "CHANGE REPLICATION SOURCE ..." statement.
+ * Added support for "CHANGE REPLICATION SOURCE ..." statement.
*/
replicationSourceOption:
(SOURCE_BIND_SYMBOL | MASTER_BIND_SYMBOL) EQUAL_OPERATOR textStringNoLinebreak
@@ -2014,11 +2014,11 @@ alterUser:
/*
* @CHANGED:
- * 1. Support also "USER()" function call.
- * 2. Fix matching "DEFAULT ROLE" by reordering rules.
- * 3. Reorder "alterUserList" and "createUserList" to match "alterUserList", as we don't handle versions yet.
+ * 1. Added support for "USER()" function call.
+ * 2. Fixed matching "DEFAULT ROLE" by reordering rules.
+ * 3. Reordered "alterUserList" and "createUserList" to match "alterUserList", as we don't handle versions yet.
* 4. Removed "IDENTIFIED (WITH) BY ..." and "discardOldPassword"; see the fixed "alterUserEntry" rule.
- * 5. Remove "FAILED_LOGIN_ATTEMPTS_SYMBOL" and "PASSWORD_LOCK_TIME_SYMBOL"; they are in "createUserTail" now.
+ * 5. Removed "FAILED_LOGIN_ATTEMPTS_SYMBOL" and "PASSWORD_LOCK_TIME_SYMBOL"; they are in "createUserTail" now.
*/
alterUserTail:
{serverVersion >= 80000}? (userFunction | user) DEFAULT_SYMBOL ROLE_SYMBOL (ALL_SYMBOL | NONE_SYMBOL | roleList)
@@ -2041,7 +2041,7 @@ createUser:
/*
* @CHANGED:
- * Add support COMMENT and ATTRIBUTE. The "(COMMENT_SYMBOL | ATTRIBUTE_SYMBOL) textString)?" was missing.
+ * Added support COMMENT and ATTRIBUTE. The "(COMMENT_SYMBOL | ATTRIBUTE_SYMBOL) textString)?" was missing.
*/
createUserTail:
{serverVersion >= 50706}? requireClause? connectOptions? accountLockPasswordExpireOptions*
@@ -2090,7 +2090,7 @@ connectOptions:
/*
* @CHANGED:
- * Add missing "PASSWORD_LOCK_TIME_SYMBOL" and "FAILED_LOGIN_ATTEMPTS_SYMBOL".
+ * Added missing "PASSWORD_LOCK_TIME_SYMBOL" and "FAILED_LOGIN_ATTEMPTS_SYMBOL".
*/
accountLockPasswordExpireOptions:
ACCOUNT_SYMBOL (LOCK_SYMBOL | UNLOCK_SYMBOL)
@@ -2180,7 +2180,7 @@ renameUser:
/*
* @CHANGED:
- * Fix "revoke" to support "IF EXISTS" and "REVOKE ALL ON ... FROM ...".
+ * Fixed "revoke" to support "IF EXISTS" and "REVOKE ALL ON ... FROM ...".
* 1. The "IF EXISTS" clause was missing in the original rule.
* 2. The "(onTypeTo? FROM_SYMBOL userList)?" part was missing in the original rule.
* 3. The "IGNORE UNKNOWN USER" clause was missing in the original rule.
@@ -2214,7 +2214,7 @@ roleOrPrivilegesList:
roleOrPrivilege:
{serverVersion > 80000}? (
- /* @CHANGED: Reorder branches to solve conflict between them. */
+ /* @CHANGED: Reordered branches to solve conflict between them. */
roleIdentifierOrText (AT_TEXT_SUFFIX | AT_SIGN_SYMBOL textOrIdentifier)
| roleIdentifierOrText columnInternalRefList?
)
@@ -2258,7 +2258,7 @@ roleOrPrivilege:
/*
* @CHANGED:
- * Rewrite "grantIdentifier" to solve conflicts between "schemaRef DOT_SYMBOL tableRef"
+ * Rewrote "grantIdentifier" to solve conflicts between "schemaRef DOT_SYMBOL tableRef"
* and "schemaRef DOT_SYMBOL MULT_OPERATOR". Move them to a single branch, and order
* "schemaRef" and "tableRef" after to preserve precedence of keywords over identifiers.
*/
@@ -2319,7 +2319,7 @@ role:
/*
* @CHANGED:
- * Fix administration statements to support both "TABLE" and "TABLES" keywords.
+ * Fixed administration statements to support both "TABLE" and "TABLES" keywords.
* The original rule only supported "TABLE".
*/
tableAdministrationStatement:
@@ -2344,7 +2344,7 @@ tableAdministrationStatement:
/*
* @CHANGED:
- * Add missing optional "USING DATA 'json_data'" to UPDATE HISTOGRAM clause.
+ * Added missing optional "USING DATA 'json_data'" to UPDATE HISTOGRAM clause.
*/
histogram:
UPDATE_SYMBOL HISTOGRAM_SYMBOL ON_SYMBOL identifierList (
@@ -2369,7 +2369,7 @@ repairType:
installUninstallStatment:
// COMPONENT_SYMBOL is conditionally set in the lexer.
action = INSTALL_SYMBOL type = PLUGIN_SYMBOL identifier SONAME_SYMBOL textStringLiteral
- /* @CHANGED: Add missing "INSTALL COMPONENT" statement "SET ..." suffix. */
+ /* @CHANGED: Added missing "INSTALL COMPONENT" statement "SET ..." suffix. */
| action = INSTALL_SYMBOL type = COMPONENT_SYMBOL textStringLiteralList (SET_SYMBOL installSetValueList)?
| action = UNINSTALL_SYMBOL type = PLUGIN_SYMBOL pluginRef
| action = UNINSTALL_SYMBOL type = COMPONENT_SYMBOL componentRef (
@@ -2379,7 +2379,7 @@ installUninstallStatment:
/*
* @CHANGED:
- * Add missing "INSTALL COMPONENT" statement "SET ..." suffix.
+ * Added missing "INSTALL COMPONENT" statement "SET ..." suffix.
*/
installOptionType: GLOBAL_SYMBOL | PERSIST_SYMBOL;
@@ -2646,7 +2646,7 @@ logType:
/*
* @CHANGED:
- * Replace "identifierList" with "tableRefList" to correctly support qualified identifiers.
+ * Replaced "identifierList" with "tableRefList" to correctly support qualified identifiers.
*/
flushTables:
(TABLES_SYMBOL | TABLE_SYMBOL) (
@@ -2739,7 +2739,7 @@ dropResourceGroup:
/*
* @CHANGED:
- * Reorder "explainStatement" and "describeStatement".
+ * Reordered "explainStatement" and "describeStatement".
* EXPLAIN can be followed by an identifier (matching a "describeStatement"),
* but identifiers can never take precedence over keywords in the grammar.
*
@@ -2775,7 +2775,7 @@ describeStatement:
/*
* @CHANGED:
- * Fix "explainStatement" to solve conflict between "ANALYZE ..." and "ANALYZE FORMAT=...".
+ * Fixed "explainStatement" to solve conflict between "ANALYZE ..." and "ANALYZE FORMAT=...".
* The "ANALYZE FORMAT=..." must be attempted to be matched before "ANALYZE ...".
*/
explainStatement:
@@ -2824,7 +2824,7 @@ restartServer:
/*
* @CHANGED:
- * Factor left recursion.
+ * Factored left recursion.
*/
expr: %expr_simple %expr_rr*;
@@ -2848,8 +2848,8 @@ expr: %expr_simple %expr_rr*;
/*
* @CHANGED:
- * 1. Factor left recursion.
- * 2. Move "compOp (ALL_SYMBOL | ANY_SYMBOL)" before "compOp predicate" to avoid conflicts.
+ * 1. Factored left recursion.
+ * 2. Movee "compOp (ALL_SYMBOL | ANY_SYMBOL)" before "compOp predicate" to avoid conflicts.
*/
boolPri:
predicate %boolPri_rr*
@@ -2905,7 +2905,7 @@ predicateOperations:
/*
* @CHANGED:
- * Factor left recursion.
+ * Factored left recursion.
*/
bitExpr: simpleExpr %bitExpr_rr*;
@@ -2955,7 +2955,7 @@ bitExpr: simpleExpr %bitExpr_rr*;
/*
* @CHANGED:
- * Factor left recursion.
+ * Factored left recursion.
*/
simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*;
@@ -2978,7 +2978,7 @@ simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*;
| OPEN_CURLY_SYMBOL identifier expr CLOSE_CURLY_SYMBOL # simpleExprOdbc
| MATCH_SYMBOL identListArg AGAINST_SYMBOL OPEN_PAR_SYMBOL bitExpr fulltextOptions? CLOSE_PAR_SYMBOL # simpleExprMatch
| BINARY_SYMBOL simpleExpr # simpleExprBinary
- /* @CHANGED: Add support for CAST(... AT TIME ZONE ... AS DATETIME ...). */
+ /* @CHANGED: Added support for CAST(... AT TIME ZONE ... AS DATETIME ...). */
| ({serverVersion >= 80022}?
CAST_SYMBOL OPEN_PAR_SYMBOL expr
AT_SYMBOL TIME_SYMBOL ZONE_SYMBOL INTERVAL_SYMBOL? textStringLiteral
@@ -2991,7 +2991,7 @@ simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*;
| DEFAULT_SYMBOL OPEN_PAR_SYMBOL simpleIdentifier CLOSE_PAR_SYMBOL # simpleExprDefault
| VALUES_SYMBOL OPEN_PAR_SYMBOL simpleIdentifier CLOSE_PAR_SYMBOL # simpleExprValues
| INTERVAL_SYMBOL expr interval PLUS_OPERATOR expr # simpleExprInterval
- /* @CHANGED: Move function calls and ref to the end to avoid conflicts with the above expressions. */
+ /* @CHANGED: Moved function calls and ref to the end to avoid conflicts with the above expressions. */
| functionCall # simpleExprFunction
| runtimeFunctionCall # simpleExprRuntimeFunction
| columnRef jsonOperator? # simpleExprColumnRef
@@ -3078,7 +3078,7 @@ windowingClause:
/*
* @CHANGED:
- * Fix "leadLagInfo" to support "identifier" and "userVariable" as well.
+ * Fixed "leadLagInfo" to support "identifier" and "userVariable" as well.
*/
leadLagInfo:
COMMA_SYMBOL (ulonglong_number | PARAM_MARKER | identifier | userVariable) (COMMA_SYMBOL expr)?
@@ -3123,7 +3123,7 @@ runtimeFunctionCall:
| name = HOUR_SYMBOL exprWithParentheses
| name = INSERT_SYMBOL OPEN_PAR_SYMBOL expr COMMA_SYMBOL expr COMMA_SYMBOL expr COMMA_SYMBOL expr CLOSE_PAR_SYMBOL
| name = INTERVAL_SYMBOL OPEN_PAR_SYMBOL expr (COMMA_SYMBOL expr)+ CLOSE_PAR_SYMBOL
- /* @CHANGED: Add support for "JSON_VALUE(..., '...' RETURNING ). */
+ /* @CHANGED: Added support for "JSON_VALUE(..., '...' RETURNING ). */
| {serverVersion >= 80021}?
name = JSON_VALUE_SYMBOL OPEN_PAR_SYMBOL simpleExpr COMMA_SYMBOL textLiteral (RETURNING_SYMBOL castType)? onEmptyOrError? CLOSE_PAR_SYMBOL
| name = LEFT_SYMBOL OPEN_PAR_SYMBOL expr COMMA_SYMBOL expr CLOSE_PAR_SYMBOL
@@ -3179,7 +3179,7 @@ runtimeFunctionCall:
| name = WEEK_SYMBOL OPEN_PAR_SYMBOL expr (COMMA_SYMBOL expr)? CLOSE_PAR_SYMBOL
| name = WEIGHT_STRING_SYMBOL OPEN_PAR_SYMBOL expr AS_SYMBOL CHAR_SYMBOL CLOSE_PAR_SYMBOL
| name = WEIGHT_STRING_SYMBOL OPEN_PAR_SYMBOL expr (
- /* @CHANGED: Move "AS BINARY(...)" before "AS CHAR(...)" to solve conflict. */
+ /* @CHANGED: Moved "AS BINARY(...)" before "AS CHAR(...)" to solve conflict. */
AS_SYMBOL BINARY_SYMBOL wsNumCodepoints
| (AS_SYMBOL CHAR_SYMBOL wsNumCodepoints)? (
{serverVersion < 80000}? weightStringLevels
@@ -3308,7 +3308,7 @@ elseExpression:
/*
* @CHANGED:
- * Fix CAST(2024 AS YEAR).
+ * Fixed CAST(2024 AS YEAR).
* The original grammar was missing the YEAR_SYMBOL in the "castType" rule.
*/
castType:
@@ -3673,7 +3673,7 @@ constraintName:
/*
* @CHANGED:
- * Fix "fieldDefinition" to solve conflict between "columnAttribute" and GENERATED/AS.
+ * Fixed "fieldDefinition" to solve conflict between "columnAttribute" and GENERATED/AS.
*/
fieldDefinition:
dataType (
@@ -3681,7 +3681,7 @@ fieldDefinition:
VIRTUAL_SYMBOL
| STORED_SYMBOL
)? (
- /* @CHANGED: Reorder "columnAttribute*" and "gcolAttribute*" to match "columnAttribute*", as we don't handle versions yet. */
+ /* @CHANGED: Reordered "columnAttribute*" and "gcolAttribute*" to match "columnAttribute*", as we don't handle versions yet. */
{serverVersion >= 80000}? columnAttribute* // Beginning with 8.0 the full attribute set is supported.
| {serverVersion < 80000}? gcolAttribute*
)
@@ -3697,7 +3697,7 @@ columnAttribute:
| NOW_SYMBOL timeFunctionParameters?
| {serverVersion >= 80013}? exprWithParentheses
)
- | {serverVersion >= 80023}? visibility /* @CHANGED: Add missing VISIBLE/INVISIBLE attribute. */
+ | {serverVersion >= 80023}? visibility /* @CHANGED: Added missing VISIBLE/INVISIBLE attribute. */
| value = ON_SYMBOL UPDATE_SYMBOL NOW_SYMBOL timeFunctionParameters?
| value = AUTO_INCREMENT_SYMBOL
| value = SERIAL_SYMBOL DEFAULT_SYMBOL VALUE_SYMBOL
@@ -3748,7 +3748,7 @@ deleteOption:
(RESTRICT_SYMBOL | CASCADE_SYMBOL)
| SET_SYMBOL nullLiteral
| NO_SYMBOL ACTION_SYMBOL
- | SET_SYMBOL DEFAULT_SYMBOL /* @CHANGED: Add missing "SET DEFAULT" option. */
+ | SET_SYMBOL DEFAULT_SYMBOL /* @CHANGED: Added missing "SET DEFAULT" option. */
;
keyList:
@@ -3962,7 +3962,7 @@ createTableOption: // In the order as they appear in the server grammar.
| REDUNDANT_SYMBOL
| COMPACT_SYMBOL
)
- /* @CHANGED: Make "tablRefList" optional. */
+ /* @CHANGED: Made "tablRefList" optional. */
| option = UNION_SYMBOL EQUAL_OPERATOR? OPEN_PAR_SYMBOL tableRefList? CLOSE_PAR_SYMBOL
| defaultCharset
| defaultCollation
@@ -3980,7 +3980,7 @@ createTableOption: // In the order as they appear in the server grammar.
| option = STORAGE_SYMBOL (DISK_SYMBOL | MEMORY_SYMBOL)
| option = CONNECTION_SYMBOL EQUAL_OPERATOR? textString
| option = KEY_BLOCK_SIZE_SYMBOL EQUAL_OPERATOR? ulong_number
- /* @CHANGED: Add missing options. */
+ /* @CHANGED: Added missing options. */
| {serverVersion >= 80021}? option = START_SYMBOL TRANSACTION_SYMBOL
| {serverVersion >= 80021}? option = ENGINE_ATTRIBUTE_SYMBOL EQUAL_OPERATOR? textString
| {serverVersion >= 80021}? option = SECONDARY_ENGINE_ATTRIBUTE_SYMBOL EQUAL_OPERATOR? textString
@@ -4120,7 +4120,7 @@ updateList:
/*
* @CHANGED:
- * Change "EQUAL_OPERATOR" to "equal" to add support for ":=".
+ * Changed "EQUAL_OPERATOR" to "equal" to add support for ":=".
*/
updateElement:
columnRef equal (expr | DEFAULT_SYMBOL)
@@ -4188,9 +4188,9 @@ createUserEntry: // create_user in sql_yacc.yy
/*
* @CHANGED:
- * Fix "alterUserEntry":
- * 1. Support also "USER()" function call.
- * 2. Add support for "RANDOM PASSWORD".
+ * Fixed "alterUserEntry":
+ * 1. Added support for "USER()" function call.
+ * 2. Added support for "RANDOM PASSWORD".
*/
alterUserEntry: // alter_user in sql_yacc.yy
(userFunction | user) (
@@ -4222,7 +4222,7 @@ replacePassword:
/*
* @CHANGED:
- * Fix "userIdentifierOrText" to support omitting sequence after "@".
+ * Fixed "userIdentifierOrText" to support omitting sequence after "@".
*/
userIdentifierOrText:
textOrIdentifier (AT_SIGN_SYMBOL textOrIdentifier? | AT_TEXT_SUFFIX)?
@@ -4598,7 +4598,7 @@ nullLiteral: // In sql_yacc.cc both 'NULL' and '\N' are mapped to NULL_SYM (whic
/*
* @CHANGED:
- * Replace "SINGLE_QUOTED_TEXT" with "textStringLiteral" to support both ' and ", as per SQL_MODE.
+ * Replaced "SINGLE_QUOTED_TEXT" with "textStringLiteral" to support both ' and ", as per SQL_MODE.
*/
temporalLiteral:
DATE_SYMBOL textStringLiteral