Skip to content

Commit

Permalink
WIP Add support for "information_schema" in CREATE TABLE
Browse files Browse the repository at this point in the history
  • Loading branch information
JanJakes committed Dec 6, 2024
1 parent b47db70 commit 58c231a
Show file tree
Hide file tree
Showing 5 changed files with 938 additions and 37 deletions.
2 changes: 1 addition & 1 deletion tests/WP_SQLite_Driver_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
226 changes: 192 additions & 34 deletions tests/WP_SQLite_Driver_Translation_Tests.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?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-token-factory.php';
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-token.php';

Expand All @@ -25,7 +26,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 {
Expand Down Expand Up @@ -206,22 +207,87 @@ 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', 0, 0, 0, 0, 0, '', '', 'int', '', '', 'select,insert,update,references', '', null, null)",
)
);
}

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')",
'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', 0, 0, 0, 0, 0, '', '', 'int', '', '', 'select,insert,update,references', '', null, 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', 0, 0, 0, 0, 0, '', '', 'text', '', '', 'select,insert,update,references', '', null, 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', 0, 0, 0, 0, 0, '', '', 'float', '', '', 'select,insert,update,references', '', null, null)",
)
);
}

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', 0, 0, 0, 0, 0, '', '', 'int', 'PRI', '', 'select,insert,update,references', '', null, null)",
)
);
}

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', 0, 0, 0, 0, 0, '', '', 'int', '', '', 'select,insert,update,references', '', null, null)",
)
);
}

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', 0, 0, 0, 0, 0, '', '', 'int', '', '', 'select,insert,update,references', '', null, null)",
)
);
}

public function testCreateTableWithPrimaryKey(): void {
/*
* PRIMARY KEY without AUTOINCREMENT:
* In this case, integer must be represented as INT, not INTEGER. SQLite
Expand All @@ -236,30 +302,80 @@ 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')",
'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', 0, 0, 0, 0, 0, '', '', 'int', 'PRI', '', 'select,insert,update,references', '', null, null)",
)
);
}

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')",
'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, 'YES', 'int', 0, 0, 0, 0, 0, '', '', 'int', 'PRI', '', 'select,insert,update,references', '', null, null)",
)
);

// 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')",
'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, 'YES', 'int', 0, 0, 0, 0, 0, '', '', 'int', 'PRI', '', 'select,insert,update,references', '', null, null)",
)
);

// 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')",
'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', 0, 0, 0, 0, 0, '', '', 'int', 'PRI', '', 'select,insert,update,references', '', null, null)",
)
);
}

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', 0, 0, 0, 0, 0, '', '', 'int', '', '', 'select,insert,update,references', '', null, null)",
)
);
}

public function testCreateTableFromSelectQuery(): void {
// CREATE TABLE AS SELECT ...
$this->assertQuery(
'CREATE TABLE "t1" AS SELECT * FROM "t2"',
Expand All @@ -272,66 +388,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'
);
}

Expand Down Expand Up @@ -465,6 +581,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
Expand All @@ -477,4 +603,36 @@ 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 ) {
if ( null === $param ) {
return 'null';
}
if ( is_string( $param ) ) {
return $this->driver->get_pdo()->quote( $param );
}
return $param;
},
$query['params']
)
);
}
$this->assertSame( $expected, $queries );
}
}
16 changes: 16 additions & 0 deletions wp-includes/parser/class-wp-parser-node.php
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,20 @@ public function get_descendant_tokens( ?int $token_id = null ): array {
}
return $all_descendants;
}

public function get_value(): 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->get_value();
}
}
return $value;
}
}
Loading

0 comments on commit 58c231a

Please sign in to comment.