From 0b7d2c309e0a98cdc4db0e163bec7377b99e045a Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Wed, 4 Dec 2024 18:10:47 +0100 Subject: [PATCH] WIP Add support for "information_schema" in CREATE TABLE --- tests/WP_SQLite_Driver_Tests.php | 2 +- tests/WP_SQLite_Driver_Translation_Tests.php | 197 ++++++++++-- wp-includes/parser/class-wp-parser-node.php | 16 + .../sqlite-ast/class-wp-sqlite-driver.php | 300 +++++++++++++++++- 4 files changed, 478 insertions(+), 37 deletions(-) diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php index 97bdf3ce..8e73f03f 100644 --- a/tests/WP_SQLite_Driver_Tests.php +++ b/tests/WP_SQLite_Driver_Tests.php @@ -39,7 +39,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/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php index 1da6334b..d43060ef 100644 --- a/tests/WP_SQLite_Driver_Translation_Tests.php +++ b/tests/WP_SQLite_Driver_Translation_Tests.php @@ -25,7 +25,7 @@ public static function setUpBeforeClass(): void { } public function setUp(): void { - $this->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 +206,73 @@ 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')", + ) + ); + } + + public function testCreateTableWithMultipleColumns(): void { $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->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')", + ) + ); + } + + 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')", + ) + ); + } + + 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')", + ) + ); + } + + 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')", + ) + ); + } + + public function testCreateTableWithPrimaryKey(): void { /* * PRIMARY KEY without AUTOINCREMENT: * In this case, integer must be represented as INT, not INTEGER. SQLite @@ -236,30 +287,70 @@ public function testCreateTable(): void { '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')", + ) + ); + } + + 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 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')", + ) ); // 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 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')", + ) ); // 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 PRIMARY KEY AUTOINCREMENT )', + 'CREATE TABLE t3 (id INT AUTO_INCREMENT, PRIMARY KEY(id))' ); - // IF NOT EXISTS. + $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')", + ) + ); + } + + 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')", + ) + ); + } + + public function testCreateTableFromSelectQuery(): void { // CREATE TABLE AS SELECT ... $this->assertQuery( 'CREATE TABLE "t1" AS SELECT * FROM "t2"', @@ -272,66 +363,66 @@ 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. - $this->assertQuery( - 'CREATE TABLE "t" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT )', - 'CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT)' - ); - - // ADD COLUMN. + public function testAlterTableAddColumn(): void { $this->assertQuery( 'ALTER TABLE "t" ADD COLUMN "a" INTEGER', 'ALTER TABLE t ADD a INT' ); + } - // ADD COLUMN with multiple columns. + public function testAlterTableAddMultipleColumns(): void { + $this->driver->get_pdo()->exec( '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', + 'ALTER TABLE "t" ADD COLUMN "a" INTEGER', + 'ALTER TABLE "t" ADD COLUMN "b" TEXT', + 'ALTER TABLE "t" ADD COLUMN "c" INTEGER', ), - '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. + public function testAlterTableDropColumn(): void { $this->assertQuery( 'ALTER TABLE "t" DROP COLUMN "a"', 'ALTER TABLE t DROP a' ); + } - // DROP COLUMN with multiple columns. + public function testAlterTableDropMultipleColumns(): void { + $this->driver->get_pdo()->exec( 'CREATE TABLE t (a INT, b INT)' ); $this->assertQuery( array( + 'ALTER TABLE "t" DROP COLUMN "a"', 'ALTER TABLE "t" DROP COLUMN "b"', - 'ALTER TABLE "t" DROP COLUMN "c"', ), - 'ALTER TABLE t DROP b, DROP c' + 'ALTER TABLE t DROP a, DROP b' ); + } - // ADD COLUMN and DROP COLUMN combined. + public function testAlterTableAddAndDropColumns(): void { + $this->driver->get_pdo()->exec( 'CREATE TABLE t (a INT)' ); $this->assertQuery( array( - 'ALTER TABLE "t" ADD COLUMN "a" INTEGER', - 'ALTER TABLE "t" DROP COLUMN "d"', + 'ALTER TABLE "t" ADD COLUMN "b" INTEGER', + 'ALTER TABLE "t" DROP COLUMN "a"', ), - 'ALTER TABLE t ADD a INT, DROP d' + 'ALTER TABLE t ADD b INT, DROP a' ); } @@ -465,6 +556,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 @@ -477,4 +578,32 @@ private function assertQuery( $expected, string $query ): void { } $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 ) { + return is_string( $param ) ? + $this->driver->get_pdo()->quote( $param ) + : $param; + }, + $query['params'] + ) + ); + } + $this->assertSame( $expected, $queries ); + } } diff --git a/wp-includes/parser/class-wp-parser-node.php b/wp-includes/parser/class-wp-parser-node.php index 89f86666..6f430bc8 100644 --- a/wp-includes/parser/class-wp-parser-node.php +++ b/wp-includes/parser/class-wp-parser-node.php @@ -262,4 +262,20 @@ public function get_descendant_tokens( ?int $token_id = null ): array { } return $all_descendants; } + + public function getValue(): string { + if ( count( $this->children ) === 0 ) { + return ''; + } + + $value = ''; + foreach ( $this->children as $child ) { + if ( $child instanceof WP_Parser_Token ) { + $value .= ' ' . $child->value; + } else { + $value .= $child->getValue(); + } + } + return $value; + } } diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index 0cb250a2..aa8bac7b 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -89,11 +89,195 @@ class WP_SQLite_Driver { PRIMARY KEY(`table`, `column_or_index`) );'; + /** + * Tables that emulate MySQL "information_schema". + * + * - TABLES + * - VIEWS + * - COLUMNS + * - STATISTICS (indexes) + * - TABLE_CONSTRAINTS (PK, UNIQUE, FK) + * - CHECK_CONSTRAINTS + * - KEY_COLUMN_USAGE (foreign keys) + * - REFERENTIAL_CONSTRAINTS (foreign keys) + * - TRIGGERS + */ + const CREATE_INFORMATION_SCHEMA_QUERIES = array( + // TABLES + "CREATE TABLE IF NOT EXISTS _mysql_information_schema_tables ( + 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, -- 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 + "CREATE TABLE IF NOT EXISTS _mysql_information_schema_columns ( + 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, -- expression for generated columns + SRS_ID INTEGER -- not implemented + ) STRICT", + + // VIEWS + 'CREATE TABLE IF NOT EXISTS _mysql_information_schema_views ( + TABLE_CATALOG TEXT NOT NULL, + TABLE_SCHEMA TEXT NOT NULL, + TABLE_NAME TEXT NOT NULL, + VIEW_DEFINITION TEXT NOT NULL, + CHECK_OPTION TEXT NOT NULL, + IS_UPDATABLE TEXT NOT NULL, + DEFINER TEXT NOT NULL, + SECURITY_TYPE TEXT NOT NULL, + CHARACTER_SET_CLIENT TEXT NOT NULL, + COLLATION_CONNECTION TEXT NOT NULL, + ALGORITHM TEXT NOT NULL + ) STRICT', + + // STATISTICS + 'CREATE TABLE IF NOT EXISTS _mysql_information_schema_statistics ( + TABLE_CATALOG TEXT NOT NULL, + 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 NOT NULL, + COLLATION TEXT, + CARDINALITY INTEGER, + SUB_PART INTEGER, + PACKED TEXT, + NULLABLE TEXT NOT NULL, + INDEX_TYPE TEXT NOT NULL, + COMMENT TEXT, + INDEX_COMMENT TEXT NOT NULL + ) STRICT', + + // TABLE_CONSTRAINTS + 'CREATE TABLE IF NOT EXISTS _mysql_information_schema_table_constraints ( + CONSTRAINT_CATALOG TEXT NOT NULL, + CONSTRAINT_SCHEMA TEXT NOT NULL, + CONSTRAINT_NAME TEXT NOT NULL, + TABLE_SCHEMA TEXT NOT NULL, + TABLE_NAME TEXT NOT NULL, + CONSTRAINT_TYPE TEXT NOT NULL + ) STRICT', + + // CHECK_CONSTRAINTS + 'CREATE TABLE IF NOT EXISTS _mysql_information_schema_check_constraints ( + CONSTRAINT_CATALOG TEXT NOT NULL, + CONSTRAINT_SCHEMA TEXT NOT NULL, + TABLE_NAME TEXT NOT NULL, + CONSTRAINT_NAME TEXT NOT NULL, + CHECK_CLAUSE TEXT NOT NULL + ) STRICT', + + // KEY_COLUMN_USAGE + 'CREATE TABLE IF NOT EXISTS _mysql_information_schema_key_column_usage ( + CONSTRAINT_CATALOG TEXT NOT NULL, + CONSTRAINT_SCHEMA TEXT NOT NULL, + CONSTRAINT_NAME TEXT NOT NULL, + TABLE_CATALOG TEXT NOT NULL, + TABLE_SCHEMA TEXT NOT NULL, + TABLE_NAME TEXT NOT NULL, + COLUMN_NAME TEXT NOT NULL, + ORDINAL_POSITION INTEGER NOT NULL, + POSITION_IN_UNIQUE_CONSTRAINT INTEGER, + REFERENCED_TABLE_SCHEMA TEXT, + REFERENCED_TABLE_NAME TEXT, + REFERENCED_COLUMN_NAME TEXT + ) STRICT', + + // REFERENTIAL_CONSTRAINTS + 'CREATE TABLE IF NOT EXISTS _mysql_information_schema_referential_constraints ( + CONSTRAINT_CATALOG TEXT NOT NULL, + CONSTRAINT_SCHEMA TEXT NOT NULL, + CONSTRAINT_NAME TEXT NOT NULL, + UNIQUE_CONSTRAINT_CATALOG TEXT NOT NULL, + UNIQUE_CONSTRAINT_SCHEMA TEXT NOT NULL, + UNIQUE_CONSTRAINT_NAME TEXT, + MATCH_OPTION TEXT NOT NULL, + UPDATE_RULE TEXT NOT NULL, + DELETE_RULE TEXT NOT NULL, + REFERENCED_TABLE_NAME TEXT NOT NULL + ) STRICT', + + // TRIGGERS + 'CREATE TABLE IF NOT EXISTS _mysql_information_schema_triggers ( + TRIGGER_CATALOG TEXT NOT NULL, + TRIGGER_SCHEMA TEXT NOT NULL, + TRIGGER_NAME TEXT NOT NULL, + EVENT_MANIPULATION TEXT NOT NULL, + EVENT_OBJECT_CATALOG TEXT NOT NULL, + EVENT_OBJECT_SCHEMA TEXT NOT NULL, + EVENT_OBJECT_TABLE TEXT NOT NULL, + ACTION_ORDER INTEGER NOT NULL, + ACTION_CONDITION TEXT, + ACTION_STATEMENT TEXT NOT NULL, + ACTION_ORIENTATION TEXT NOT NULL, + ACTION_TIMING TEXT NOT NULL, + ACTION_REFERENCE_OLD_TABLE TEXT, + ACTION_REFERENCE_NEW_TABLE TEXT, + ACTION_REFERENCE_OLD_ROW TEXT NOT NULL, + ACTION_REFERENCE_NEW_ROW TEXT NOT NULL, + CREATED TEXT, + SQL_MODE TEXT NOT NULL, + DEFINER TEXT NOT NULL, + CHARACTER_SET_CLIENT TEXT NOT NULL, + COLLATION_CONNECTION TEXT NOT NULL, + DATABASE_COLLATION TEXT NOT NULL + ) STRICT', + ); + /** * @var WP_Parser_Grammar */ 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. * @@ -254,9 +438,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(); @@ -304,6 +490,10 @@ public function __construct( $pdo = null ) { $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO $pdo->query( self::CREATE_DATA_TYPES_CACHE_TABLE ); + foreach ( self::CREATE_INFORMATION_SCHEMA_QUERIES as $query ) { + $pdo->query( $query ); + } + /* * A list of system tables lets us emulate information_schema * queries without returning extra tables. @@ -962,6 +1152,110 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void { $this->execute_sqlite_query( implode( ' ', $query_parts ) ); $this->set_result_from_affected_rows(); + + // Save information to "information_schema" tables. + $table_name = $this->translate( $node->get_descendant_node( 'tableName' ) ); + $table_name = trim( $table_name, '`"' ); + + // 1. INFORMATION_SCHEMA.TABLES: + $engine = $this->translate( $node->get_descendant_node( 'engineRef' ) ) ?? 'InnoDB'; + $engine = strtoupper( trim( $engine, '`"' ) ); + + $row_format = 'Dynamic'; + if ( 'INNODB' === $engine ) { + $engine = 'InnoDB'; + } elseif ( 'MYISAM' === $engine ) { + $engine = 'MyISAM'; + $row_format = 'Fixed'; + } + + $collate = $this->translate( $node->get_descendant_node( 'collationName' ) ) ?? 'utf8mb4_general_ci'; + $collate = strtolower( trim( $collate, '`"' ) ); + + $this->execute_sqlite_query( + ' + INSERT INTO _mysql_information_schema_tables (table_schema, table_name, table_type, engine, row_format, table_collation) + VALUES (?, ?, ?, ?, ?, ?) + ', + array( + $this->db_name, + $table_name, + 'BASE TABLE', + $engine, + $row_format, + $collate, + ) + ); + + // 2. INFORMATION_SCHEMA.COLUMNS: + $position = 1; + foreach ( $columns as $column ) { + $column_name = $column->get_child_node( 'columnName' )->getValue(); + $column_name = trim( $column_name, '`"' ); + + $column_type = $column->get_descendant_node( 'dataType' )->getValue(); + $data_type = $column_type; // @TODO: Extract pure data type. + + $column_default = $column->get_descendant_node( 'defaultClause' ); + if ( null !== $column_default ) { + $column_default = $column_default->getValue(); + } + + $column_nullable = 'YES'; + foreach ( $column->get_descendant_nodes( 'columnAttribute' ) as $attr ) { + if ( + null !== $attr->get_descendant_token( WP_MySQL_Lexer::NOT_SYMBOL ) + && null !== $attr->get_descendant_token( WP_MySQL_Lexer::NULL_SYMBOL ) + ) { + $column_nullable = 'NO'; + break; + } + } + + $column_key = ''; + /*if ( $column->has_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) { + $column_key = 'PRI'; + } elseif ( $column->has_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ) ) { + $column_key = 'UNI'; + }*/ + + $column_extra = ''; + /*if ( $column->has_descendant_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) ) { + $column_extra = 'auto_increment'; + }*/ + + $data = array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + 'column_name' => $column_name, + 'ordinal_position' => $position, + 'column_default' => $column_default, + 'is_nullable' => $column_nullable, + 'data_type' => $data_type, + 'character_maximum_length' => 0, // @TODO + 'character_octet_length' => 0, // @TODO + 'numeric_precision' => 0, // @TODO + 'numeric_scale' => 0, // @TODO + 'datetime_precision' => 0, // @TODO + 'character_set_name' => '', // @TODO + 'collation_name' => '', // @TODO + 'column_type' => $column_type, + 'column_key' => '', // @TODO + 'extra' => '', // @TODO + 'privileges' => '', // @TODO + 'column_comment' => '', // @TODO + 'generation_expression' => null, // @TODO + 'srs_id' => null, // @TODO + ); + + $this->execute_sqlite_query( + ' + INSERT INTO _mysql_information_schema_columns (' . implode( ', ', array_keys( $data ) ) . ') + VALUES (' . implode( ', ', array_fill( 0, count( $data ), '?' ) ) . ') + ', + array_values( $data ) + ); + } } private function execute_alter_table_statement( WP_Parser_Node $node ): void { @@ -1149,6 +1443,8 @@ private function translate( $ast ) { return null; } return $this->translate_sequence( $ast->get_children() ); + case 'defaultCollation': + return null; case 'duplicateAsQueryExpression': // @TODO: How to handle IGNORE/REPLACE?