diff --git a/tests/WP_SQLite_Driver_Metadata_Tests.php b/tests/WP_SQLite_Driver_Metadata_Tests.php index 23c287f..de1e686 100644 --- a/tests/WP_SQLite_Driver_Metadata_Tests.php +++ b/tests/WP_SQLite_Driver_Metadata_Tests.php @@ -458,4 +458,358 @@ public function testBogusQuery() { 'SELECT 1, BOGUS(1) FROM bogus;' ); } + + public function testInformationSchemaTableConstraintsCreateTable(): void { + $this->assertQuery( + 'CREATE TABLE t ( + a INT PRIMARY KEY, + b INT UNIQUE, + c INT, + d INT, + CONSTRAINT unique_b_c UNIQUE (b, c), + INDEX inex_c_d (c, d) + )' + ); + + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'PRIMARY', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'PRIMARY KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'b', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'UNIQUE', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'unique_b_c', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'UNIQUE', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + } + + public function testInformationSchemaTableConstraintsDropTable(): void { + $this->assertQuery( 'CREATE TABLE t (a INT PRIMARY KEY, b INT UNIQUE)' ); + $this->assertQuery( 'DROP TABLE t' ); + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( array(), $result ); + } + + public function testInformationSchemaTableConstraintsAddColumn(): void { + $this->assertQuery( + 'CREATE TABLE t ( a INT )' + ); + + // Add a column with a primary key constraint. + $this->assertQuery( 'ALTER TABLE t ADD COLUMN b INT PRIMARY KEY' ); + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'PRIMARY', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'PRIMARY KEY', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + + $this->assertQuery( 'ALTER TABLE t ADD COLUMN c INT UNIQUE' ); + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'PRIMARY', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'PRIMARY KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'c', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'UNIQUE', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + } + + public function testInformationSchemaTableConstraintsChangeColumn(): void { + $this->assertQuery( 'CREATE TABLE t (a INT, b INT)' ); + + // Add a primary key constraint. + $this->assertQuery( 'ALTER TABLE t CHANGE COLUMN a a INT PRIMARY KEY' ); + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'PRIMARY', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'PRIMARY KEY', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + + // Add a unique constraint. + $this->assertQuery( 'ALTER TABLE t MODIFY COLUMN b INT UNIQUE' ); + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'PRIMARY', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'PRIMARY KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'b', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'UNIQUE', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + } + + public function testInformationSchemaTableConstraintsDropColumn(): void { + $this->assertQuery( + 'CREATE TABLE t ( + id INT, + a INT, + b INT, + c INT, + CONSTRAINT c_primary PRIMARY KEY (a, b), + CONSTRAINT c_unique UNIQUE (b, c), + INDEX id (a, b, c) + )' + ); + + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'PRIMARY', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'PRIMARY KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'c_unique', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'UNIQUE', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + + // Drop column "b" - all constraints will remain. + $this->assertQuery( 'ALTER TABLE t DROP COLUMN b' ); + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'PRIMARY', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'PRIMARY KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'c_unique', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'UNIQUE', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + + // Drop column "c" - the unique constraint will be removed. + $this->assertQuery( 'ALTER TABLE t DROP COLUMN c' ); + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'PRIMARY', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'PRIMARY KEY', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + + // Drop column "a" - the primary key will be removed. + $this->assertQuery( 'ALTER TABLE t DROP COLUMN a' ); + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( array(), $result ); + } + + public function testInformationSchemaTableConstraintsAddConstraint(): void { + $this->assertQuery( 'CREATE TABLE t (a INT, b INT)' ); + + // Add a primary key constraint. + $this->assertQuery( 'ALTER TABLE t ADD CONSTRAINT primary_key_a PRIMARY KEY (a)' ); + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'PRIMARY', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'PRIMARY KEY', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + + // Add a unique constraint. + $this->assertQuery( 'ALTER TABLE t ADD CONSTRAINT unique_b UNIQUE (b)' ); + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'PRIMARY', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'PRIMARY KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'unique_b', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'UNIQUE', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + + // Add a unique constraint with a composite key. + $this->assertQuery( 'ALTER TABLE t ADD CONSTRAINT unique_a_b UNIQUE (a, b)' ); + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'PRIMARY', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'PRIMARY KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'unique_b', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'UNIQUE', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'unique_a_b', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'UNIQUE', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + } + + public function testInformationSchemaTableConstraintsDropIndex(): void { + $this->assertQuery( 'CREATE TABLE t (a INT PRIMARY KEY, b INT UNIQUE)' ); + + // Drop the primary key index. + $this->assertQuery( 'ALTER TABLE t DROP INDEX `PRIMARY`' ); + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'b', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't', + 'CONSTRAINT_TYPE' => 'UNIQUE', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + + // Drop the unique index. + $this->assertQuery( 'ALTER TABLE t DROP INDEX b' ); + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertEquals( array(), $result ); + } } diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php index e55c759..26f05bd 100644 --- a/tests/WP_SQLite_Driver_Tests.php +++ b/tests/WP_SQLite_Driver_Tests.php @@ -4704,6 +4704,107 @@ public function testAlterTableDuplicateKeyNameWithUnique(): void { $this->assertSame( '42S21', $exception->getCode() ); } + public function testConstraintName(): void { + $this->assertQuery( + 'CREATE TABLE t ( id INT, CONSTRAINT cst_id UNIQUE (id) )' + ); + + $result = $this->assertQuery( 'SHOW INDEX FROM t' ); + $this->assertCount( 1, $result ); + $this->assertSame( 'cst_id', $result[0]->Key_name ); + } + + public function testIndexNamePrecedesConstraintName(): void { + $this->assertQuery( + 'CREATE TABLE t ( id INT, CONSTRAINT cst_id UNIQUE idx_id (id) )' + ); + + $result = $this->assertQuery( 'SHOW INDEX FROM t' ); + $this->assertCount( 1, $result ); + $this->assertSame( 'idx_id', $result[0]->Key_name ); + + $result = $this->assertQuery( 'SHOW CREATE TABLE t' ); + $this->assertCount( 1, $result ); + $this->assertSame( + implode( + "\n", + array( + 'CREATE TABLE `t` (', + ' `id` int DEFAULT NULL,', + ' UNIQUE KEY `idx_id` (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci', + ) + ), + $result[0]->{'Create Table'} + ); + } + + public function testImplicitIndexNames(): void { + $this->assertQuery( + 'CREATE TABLE t ( + id INT UNIQUE, + id_2 INT UNIQUE, + value INT, + UNIQUE (id), + UNIQUE (id, value) + )' + ); + + $result = $this->assertQuery( 'SHOW INDEX FROM t' ); + $this->assertCount( 5, $result ); + + $this->assertSame( 'id', $result[0]->Key_name ); + $this->assertSame( 'id', $result[0]->Column_name ); + + $this->assertSame( 'id_2', $result[1]->Key_name ); + $this->assertSame( 'id_2', $result[1]->Column_name ); + + $this->assertSame( 'id_3', $result[2]->Key_name ); + $this->assertSame( 'id', $result[2]->Column_name ); + + $this->assertSame( 'id_4', $result[3]->Key_name ); + $this->assertSame( 'id', $result[3]->Column_name ); + + $this->assertSame( 'id_4', $result[4]->Key_name ); + $this->assertSame( 'value', $result[4]->Column_name ); + } + + public function testValidDuplicateConstraintNames(): void { + $this->assertQuery( + 'CREATE TABLE t ( + id INT, + CONSTRAINT cid PRIMARY KEY (id), + CONSTRAINT cid UNIQUE (id) + -- Not yet supported: CONSTRAINT cid CHECK (id > 0), + -- Not yet supported: CONSTRAINT cid FOREIGN KEY (id) REFERENCES t (id) + )' + ); + + // No exception. This table definition is valid in MySQL. + // Constraint names must be unique per constraint type, not per table. + } + + public function testMultipleTablesWithSameConstraintNames(): void { + $this->assertQuery( + 'CREATE TABLE t1 ( + id INT, + CONSTRAINT c_primary PRIMARY KEY (id), + CONSTRAINT c_unique UNIQUE (id) + )' + ); + + $this->assertQuery( + 'CREATE TABLE t2 ( + id INT, + CONSTRAINT c_primary PRIMARY KEY (id), + CONSTRAINT c_unique UNIQUE (id) + )' + ); + + // No exception. This is valid in MySQL. + // Primary and unique key names must be unique per table, not per schema. + } + public function testNoBackslashEscapesSqlMode(): void { $backslash = chr( 92 ); @@ -5004,4 +5105,86 @@ function ( string $utf8_literal ) { $quote( chr( 0xC0 ) . chr( 39 ) ) ); } + + public function testColumnNamesAreNotCaseSensitive(): void { + $this->assertQuery( 'CREATE TABLE t (value TEXT)' ); + + // INSERT. + $this->assertQuery( "INSERT INTO t (value) VALUES ('one')" ); + $this->assertQuery( "INSERT INTO t (VaLuE) VALUES ('two')" ); + $result = $this->assertQuery( 'SELECT * FROM t' ); + $this->assertCount( 2, $result ); + + // SELECT. + $result = $this->assertQuery( "SELECT * FROM t WHERE value = 'one'" ); + $this->assertCount( 1, $result ); + $this->assertSame( 'one', $result[0]->value ); + + $result = $this->assertQuery( "SELECT * FROM t WHERE VaLuE = 'two'" ); + $this->assertCount( 1, $result ); + $this->assertSame( 'two', $result[0]->value ); + + // UPDATE. + $this->assertQuery( "UPDATE t SET value = 'one-updated' WHERE value = 'one'" ); + $result = $this->assertQuery( "SELECT * FROM t WHERE value = 'one-updated'" ); + $this->assertCount( 1, $result ); + $this->assertSame( 'one-updated', $result[0]->value ); + + $this->assertQuery( "UPDATE t SET VALUE = 'two-updated' WHERE VaLuE = 'two'" ); + $result = $this->assertQuery( "SELECT * FROM t WHERE value = 'two-updated'" ); + $this->assertCount( 1, $result ); + $this->assertSame( 'two-updated', $result[0]->value ); + + // DELETE. + $this->assertQuery( "DELETE FROM t WHERE value = 'one-updated'" ); + $result = $this->assertQuery( 'SELECT * FROM t' ); + $this->assertCount( 1, $result ); + $this->assertSame( 'two-updated', $result[0]->value ); + + $this->assertQuery( "DELETE FROM t WHERE VaLuE = 'two-updated'" ); + $result = $this->assertQuery( 'SELECT * FROM t' ); + $this->assertCount( 0, $result ); + + // ALTER TABLE. + $this->assertQuery( 'ALTER TABLE t CHANGE COLUMN VaLuE value_changed TEXT' ); + $this->assertQuery( 'ALTER TABLE t CHANGE COLUMN value_changed value TEXT' ); + + // ADD COLUMN. + $this->assertQuery( 'ALTER TABLE t ADD COLUMN added TEXT' ); + $exception = null; + try { + $this->assertQuery( 'ALTER TABLE t ADD COLUMN AdDeD TEXT' ); + } catch ( Throwable $e ) { + $exception = $e; + } + $this->assertNotNull( $exception ); + $this->assertStringContainsString( + "Column already exists: 1060 Duplicate column name 'AdDeD'", + $exception->getMessage() + ); + + // DROP COLUMN. + $this->assertQuery( 'ALTER TABLE t DROP COLUMN added' ); + $result = $this->assertQuery( 'SHOW COLUMNS FROM t' ); + $this->assertCount( 1, $result ); + $this->assertSame( 'value', $result[0]->Field ); + } + + public function testAliasesMustBeAscii(): void { + $this->expectException( WP_SQLite_Driver_Exception::class ); + $this->expectExceptionMessage( 'The SQLite driver only supports ASCII characters in identifiers.' ); + $this->assertQuery( 'SELECT 123 AS `ńôñ-ášçíì`' ); + } + + public function testTableNamesMustBeAscii(): void { + $this->expectException( WP_SQLite_Driver_Exception::class ); + $this->expectExceptionMessage( 'The SQLite driver only supports ASCII characters in identifiers.' ); + $this->assertQuery( 'CREATE TABLE `ńôñ-ášçíì` (id INT)' ); + } + + public function testColumnNamesMustBeAscii(): void { + $this->expectException( WP_SQLite_Driver_Exception::class ); + $this->expectExceptionMessage( 'The SQLite driver only supports ASCII characters in identifiers.' ); + $this->assertQuery( 'CREATE TABLE t (`ńôñ-ášçíì` INT)' ); + } } diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php index 28ad29e..8c3f909 100644 --- a/tests/WP_SQLite_Driver_Translation_Tests.php +++ b/tests/WP_SQLite_Driver_Translation_Tests.php @@ -254,6 +254,8 @@ 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 `_wp_sqlite_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)", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' + . " VALUES ('wp', 't', 'wp', 'PRIMARY', 'PRIMARY KEY')", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -324,6 +326,8 @@ 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 `_wp_sqlite_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)", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' + . " VALUES ('wp', 't', 'wp', 'PRIMARY', 'PRIMARY KEY')", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -346,6 +350,8 @@ 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 `_wp_sqlite_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)", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' + . " VALUES ('wp', 't1', 'wp', 'PRIMARY', 'PRIMARY KEY')", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't1'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't1' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't1' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -366,6 +372,8 @@ 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 `_wp_sqlite_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)", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' + . " VALUES ('wp', 't2', 'wp', 'PRIMARY', 'PRIMARY KEY')", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't2'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't2' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't2' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -387,7 +395,9 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void { "SELECT column_name, data_type, is_nullable, character_maximum_length FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't3' AND column_name IN ('id')", 'INSERT INTO `_wp_sqlite_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 `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't3' GROUP BY column_name ) UPDATE `_wp_sqlite_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", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' + . " VALUES ('wp', 't3', 'wp', 'PRIMARY', 'PRIMARY KEY')", + "UPDATE `_wp_sqlite_mysql_information_schema_columns` AS c SET (column_key, is_nullable) = ( SELECT CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(s.non_unique = 0 AND s.seq_in_index = 1) THEN 'UNI' WHEN MAX(s.seq_in_index = 1) THEN 'MUL' ELSE '' END, CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'NO' ELSE c.is_nullable END FROM `_wp_sqlite_mysql_information_schema_statistics` AS s WHERE s.table_schema = c.table_schema AND s.table_name = c.table_name AND s.column_name = c.column_name ) WHERE c.table_schema = 'wp' AND c.table_name = 't3'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't3'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't3' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't3' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -413,10 +423,14 @@ public function testCreateTableWithInlineUniqueIndexes(): void { . " VALUES ('wp', 't', 'id', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', 'UNI', '', 'select,insert,update,references', '', '', null)", 'INSERT INTO `_wp_sqlite_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 `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' + . " VALUES ('wp', 't', 'wp', 'id', 'UNIQUE')", 'INSERT INTO `_wp_sqlite_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 `_wp_sqlite_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)", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' + . " VALUES ('wp', 't', 'wp', 'name', 'UNIQUE')", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -443,13 +457,19 @@ public function testCreateTableWithStandaloneUniqueIndexes(): void { 'INSERT INTO `_wp_sqlite_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 `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' AND column_name IN ('id')", + "SELECT DISTINCT index_name FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' AND (index_name = 'id' OR index_name LIKE 'id\_%' ESCAPE '\\')", 'INSERT INTO `_wp_sqlite_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 `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE `_wp_sqlite_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", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' + . " VALUES ('wp', 't', 'wp', 'id', 'UNIQUE')", + "UPDATE `_wp_sqlite_mysql_information_schema_columns` AS c SET (column_key, is_nullable) = ( SELECT CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(s.non_unique = 0 AND s.seq_in_index = 1) THEN 'UNI' WHEN MAX(s.seq_in_index = 1) THEN 'MUL' ELSE '' END, CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'NO' ELSE c.is_nullable END FROM `_wp_sqlite_mysql_information_schema_statistics` AS s WHERE s.table_schema = c.table_schema AND s.table_name = c.table_name AND s.column_name = c.column_name ) WHERE c.table_schema = 'wp' AND c.table_name = 't'", "SELECT column_name, data_type, is_nullable, character_maximum_length FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' AND column_name IN ('name')", + "SELECT DISTINCT index_name FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' AND (index_name = 'name' OR index_name LIKE 'name\_%' ESCAPE '\\')", 'INSERT INTO `_wp_sqlite_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 `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE `_wp_sqlite_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", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' + . " VALUES ('wp', 't', 'wp', 'name', 'UNIQUE')", + "UPDATE `_wp_sqlite_mysql_information_schema_columns` AS c SET (column_key, is_nullable) = ( SELECT CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(s.non_unique = 0 AND s.seq_in_index = 1) THEN 'UNI' WHEN MAX(s.seq_in_index = 1) THEN 'MUL' ELSE '' END, CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'NO' ELSE c.is_nullable END FROM `_wp_sqlite_mysql_information_schema_statistics` AS s WHERE s.table_schema = c.table_schema AND s.table_name = c.table_name AND s.column_name = c.column_name ) WHERE c.table_schema = 'wp' AND c.table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -514,7 +534,7 @@ public function testAlterTableAddColumn(): void { $this->assertExecutedInformationSchemaQueries( array( - "SELECT COLUMN_NAME FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT COLUMN_NAME, LOWER(COLUMN_NAME) AS COLUMN_NAME_LOWERCASE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", "SELECT MAX(ordinal_position) FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", 'INSERT INTO `_wp_sqlite_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)", @@ -543,7 +563,7 @@ public function testAlterTableAddColumnWithNotNull(): void { $this->assertExecutedInformationSchemaQueries( array( - "SELECT COLUMN_NAME FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT COLUMN_NAME, LOWER(COLUMN_NAME) AS COLUMN_NAME_LOWERCASE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", "SELECT MAX(ordinal_position) FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", 'INSERT INTO `_wp_sqlite_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)", @@ -572,7 +592,7 @@ public function testAlterTableAddColumnWithDefault(): void { $this->assertExecutedInformationSchemaQueries( array( - "SELECT COLUMN_NAME FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT COLUMN_NAME, LOWER(COLUMN_NAME) AS COLUMN_NAME_LOWERCASE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", "SELECT MAX(ordinal_position) FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", 'INSERT INTO `_wp_sqlite_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)", @@ -601,7 +621,7 @@ public function testAlterTableAddColumnWithNotNullAndDefault(): void { $this->assertExecutedInformationSchemaQueries( array( - "SELECT COLUMN_NAME FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT COLUMN_NAME, LOWER(COLUMN_NAME) AS COLUMN_NAME_LOWERCASE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", "SELECT MAX(ordinal_position) FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", 'INSERT INTO `_wp_sqlite_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)", @@ -630,7 +650,7 @@ public function testAlterTableAddMultipleColumns(): void { $this->assertExecutedInformationSchemaQueries( array( - "SELECT COLUMN_NAME FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT COLUMN_NAME, LOWER(COLUMN_NAME) AS COLUMN_NAME_LOWERCASE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", "SELECT MAX(ordinal_position) FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", 'INSERT INTO `_wp_sqlite_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)", @@ -665,10 +685,12 @@ public function testAlterTableDropColumn(): void { $this->assertExecutedInformationSchemaQueries( array( - "SELECT COLUMN_NAME FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT COLUMN_NAME, LOWER(COLUMN_NAME) AS COLUMN_NAME_LOWERCASE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", "DELETE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE `table_schema` = 'wp' AND `table_name` = 't' AND `column_name` = 'a'", "DELETE FROM `_wp_sqlite_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 `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE `_wp_sqlite_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", + "UPDATE `_wp_sqlite_mysql_information_schema_statistics` AS statistics SET seq_in_index = renumbered.seq_in_index FROM ( SELECT rowid, row_number() OVER (PARTITION BY index_name ORDER BY seq_in_index) AS seq_in_index FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ) AS renumbered WHERE statistics.rowid = renumbered.rowid AND statistics.seq_in_index != renumbered.seq_in_index", + "UPDATE `_wp_sqlite_mysql_information_schema_columns` AS c SET (column_key, is_nullable) = ( SELECT CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(s.non_unique = 0 AND s.seq_in_index = 1) THEN 'UNI' WHEN MAX(s.seq_in_index = 1) THEN 'MUL' ELSE '' END, CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'NO' ELSE c.is_nullable END FROM `_wp_sqlite_mysql_information_schema_statistics` AS s WHERE s.table_schema = c.table_schema AND s.table_name = c.table_name AND s.column_name = c.column_name ) WHERE c.table_schema = 'wp' AND c.table_name = 't'", + "DELETE FROM `_wp_sqlite_mysql_information_schema_table_constraints` WHERE table_schema = 'wp' AND table_name = 't' AND constraint_type IN ('PRIMARY KEY', 'UNIQUE') AND constraint_name NOT IN ( SELECT DISTINCT index_name FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' )", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -693,13 +715,17 @@ public function testAlterTableDropMultipleColumns(): void { $this->assertExecutedInformationSchemaQueries( array( - "SELECT COLUMN_NAME FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT COLUMN_NAME, LOWER(COLUMN_NAME) AS COLUMN_NAME_LOWERCASE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", "DELETE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE `table_schema` = 'wp' AND `table_name` = 't' AND `column_name` = 'a'", "DELETE FROM `_wp_sqlite_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 `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE `_wp_sqlite_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", + "UPDATE `_wp_sqlite_mysql_information_schema_statistics` AS statistics SET seq_in_index = renumbered.seq_in_index FROM ( SELECT rowid, row_number() OVER (PARTITION BY index_name ORDER BY seq_in_index) AS seq_in_index FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ) AS renumbered WHERE statistics.rowid = renumbered.rowid AND statistics.seq_in_index != renumbered.seq_in_index", + "UPDATE `_wp_sqlite_mysql_information_schema_columns` AS c SET (column_key, is_nullable) = ( SELECT CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(s.non_unique = 0 AND s.seq_in_index = 1) THEN 'UNI' WHEN MAX(s.seq_in_index = 1) THEN 'MUL' ELSE '' END, CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'NO' ELSE c.is_nullable END FROM `_wp_sqlite_mysql_information_schema_statistics` AS s WHERE s.table_schema = c.table_schema AND s.table_name = c.table_name AND s.column_name = c.column_name ) WHERE c.table_schema = 'wp' AND c.table_name = 't'", + "DELETE FROM `_wp_sqlite_mysql_information_schema_table_constraints` WHERE table_schema = 'wp' AND table_name = 't' AND constraint_type IN ('PRIMARY KEY', 'UNIQUE') AND constraint_name NOT IN ( SELECT DISTINCT index_name FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' )", "DELETE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE `table_schema` = 'wp' AND `table_name` = 't' AND `column_name` = 'b'", "DELETE FROM `_wp_sqlite_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 `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE `_wp_sqlite_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", + "UPDATE `_wp_sqlite_mysql_information_schema_statistics` AS statistics SET seq_in_index = renumbered.seq_in_index FROM ( SELECT rowid, row_number() OVER (PARTITION BY index_name ORDER BY seq_in_index) AS seq_in_index FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ) AS renumbered WHERE statistics.rowid = renumbered.rowid AND statistics.seq_in_index != renumbered.seq_in_index", + "UPDATE `_wp_sqlite_mysql_information_schema_columns` AS c SET (column_key, is_nullable) = ( SELECT CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(s.non_unique = 0 AND s.seq_in_index = 1) THEN 'UNI' WHEN MAX(s.seq_in_index = 1) THEN 'MUL' ELSE '' END, CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'NO' ELSE c.is_nullable END FROM `_wp_sqlite_mysql_information_schema_statistics` AS s WHERE s.table_schema = c.table_schema AND s.table_name = c.table_name AND s.column_name = c.column_name ) WHERE c.table_schema = 'wp' AND c.table_name = 't'", + "DELETE FROM `_wp_sqlite_mysql_information_schema_table_constraints` WHERE table_schema = 'wp' AND table_name = 't' AND constraint_type IN ('PRIMARY KEY', 'UNIQUE') AND constraint_name NOT IN ( SELECT DISTINCT index_name FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' )", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -725,13 +751,15 @@ public function testAlterTableAddAndDropColumns(): void { $this->assertExecutedInformationSchemaQueries( array( - "SELECT COLUMN_NAME FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT COLUMN_NAME, LOWER(COLUMN_NAME) AS COLUMN_NAME_LOWERCASE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", "SELECT MAX(ordinal_position) FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", 'INSERT INTO `_wp_sqlite_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 `_wp_sqlite_mysql_information_schema_columns` WHERE `table_schema` = 'wp' AND `table_name` = 't' AND `column_name` = 'a'", "DELETE FROM `_wp_sqlite_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 `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE `_wp_sqlite_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", + "UPDATE `_wp_sqlite_mysql_information_schema_statistics` AS statistics SET seq_in_index = renumbered.seq_in_index FROM ( SELECT rowid, row_number() OVER (PARTITION BY index_name ORDER BY seq_in_index) AS seq_in_index FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ) AS renumbered WHERE statistics.rowid = renumbered.rowid AND statistics.seq_in_index != renumbered.seq_in_index", + "UPDATE `_wp_sqlite_mysql_information_schema_columns` AS c SET (column_key, is_nullable) = ( SELECT CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(s.non_unique = 0 AND s.seq_in_index = 1) THEN 'UNI' WHEN MAX(s.seq_in_index = 1) THEN 'MUL' ELSE '' END, CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'NO' ELSE c.is_nullable END FROM `_wp_sqlite_mysql_information_schema_statistics` AS s WHERE s.table_schema = c.table_schema AND s.table_name = c.table_name AND s.column_name = c.column_name ) WHERE c.table_schema = 'wp' AND c.table_name = 't'", + "DELETE FROM `_wp_sqlite_mysql_information_schema_table_constraints` WHERE table_schema = 'wp' AND table_name = 't' AND constraint_type IN ('PRIMARY KEY', 'UNIQUE') AND constraint_name NOT IN ( SELECT DISTINCT index_name FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' )", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -757,10 +785,12 @@ public function testAlterTableDropAndAddSingleColumn(): void { $this->assertExecutedInformationSchemaQueries( array( - "SELECT COLUMN_NAME FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", + "SELECT COLUMN_NAME, LOWER(COLUMN_NAME) AS COLUMN_NAME_LOWERCASE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", "DELETE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE `table_schema` = 'wp' AND `table_name` = 't' AND `column_name` = 'a'", "DELETE FROM `_wp_sqlite_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 `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' GROUP BY column_name ) UPDATE `_wp_sqlite_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", + "UPDATE `_wp_sqlite_mysql_information_schema_statistics` AS statistics SET seq_in_index = renumbered.seq_in_index FROM ( SELECT rowid, row_number() OVER (PARTITION BY index_name ORDER BY seq_in_index) AS seq_in_index FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ) AS renumbered WHERE statistics.rowid = renumbered.rowid AND statistics.seq_in_index != renumbered.seq_in_index", + "UPDATE `_wp_sqlite_mysql_information_schema_columns` AS c SET (column_key, is_nullable) = ( SELECT CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(s.non_unique = 0 AND s.seq_in_index = 1) THEN 'UNI' WHEN MAX(s.seq_in_index = 1) THEN 'MUL' ELSE '' END, CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'NO' ELSE c.is_nullable END FROM `_wp_sqlite_mysql_information_schema_statistics` AS s WHERE s.table_schema = c.table_schema AND s.table_name = c.table_name AND s.column_name = c.column_name ) WHERE c.table_schema = 'wp' AND c.table_name = 't'", + "DELETE FROM `_wp_sqlite_mysql_information_schema_table_constraints` WHERE table_schema = 'wp' AND table_name = 't' AND constraint_type IN ('PRIMARY KEY', 'UNIQUE') AND constraint_name NOT IN ( SELECT DISTINCT index_name FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' )", "SELECT MAX(ordinal_position) FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't'", 'INSERT INTO `_wp_sqlite_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)", diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index e8d176e..613ac18 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -1286,14 +1286,20 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void { $columns_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'columns' ); $column_names = $this->execute_sqlite_query( sprintf( - 'SELECT COLUMN_NAME FROM %s WHERE table_schema = ? AND table_name = ?', + 'SELECT + COLUMN_NAME, + LOWER(COLUMN_NAME) AS COLUMN_NAME_LOWERCASE + FROM %s WHERE table_schema = ? AND table_name = ?', $this->quote_sqlite_identifier( $columns_table ) ), array( $this->db_name, $table_name ) - )->fetchAll( PDO::FETCH_COLUMN ); + )->fetchAll( PDO::FETCH_ASSOC ); // Track column renames and removals. - $column_map = array_combine( $column_names, $column_names ); + $column_map = array_combine( + array_column( $column_names, 'COLUMN_NAME_LOWERCASE' ), + array_column( $column_names, 'COLUMN_NAME' ) + ); foreach ( $node->get_descendant_nodes( 'alterListItem' ) as $action ) { $first_token = $action->get_first_child_token(); @@ -1302,7 +1308,7 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void { $name = $this->translate( $action->get_first_child_node( 'fieldIdentifier' ) ); if ( null !== $name ) { $name = $this->unquote_sqlite_identifier( $name ); - unset( $column_map[ $name ] ); + unset( $column_map[ strtolower( $name ) ] ); } break; case WP_MySQL_Lexer::CHANGE_SYMBOL: @@ -1313,7 +1319,7 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void { $this->translate( $action->get_first_child_node( 'identifier' ) ) ); - $column_map[ $old_name ] = $new_name; + $column_map[ strtolower( $old_name ) ] = $new_name; break; case WP_MySQL_Lexer::RENAME_SYMBOL: $column_ref = $action->get_first_child_node( 'fieldIdentifier' ); @@ -1325,7 +1331,7 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void { $this->translate( $action->get_first_child_node( 'identifier' ) ) ); - $column_map[ $old_name ] = $new_name; + $column_map[ strtolower( $old_name ) ] = $new_name; } break; } @@ -2115,7 +2121,21 @@ private function translate( $node ): ?string { case 'identifierKeyword': return '`' . $this->translate( $node->get_first_child() ) . '`'; case 'pureIdentifier': - return $this->translate_pure_identifier( $node ); + $value = $this->translate_pure_identifier( $node ); + + /* + * At the moment, we only support ASCII bytes in all identifiers. + * This is because SQLite doesn't support case-insensitive Unicode + * character matching: https://sqlite.org/faq.html#q18 + */ + for ( $i = 0; $i < strlen( $value ); $i++ ) { + if ( ord( $value[ $i ] ) > 127 ) { + throw $this->new_driver_exception( + 'The SQLite driver only supports ASCII characters in identifiers.' + ); + } + } + return $value; case 'textStringLiteral': return $this->translate_string_literal( $node ); case 'dataType': @@ -3045,7 +3065,7 @@ private function translate_update_list_in_non_strict_mode( string $table_name, W $columns_table = $this->information_schema_builder->get_table_name( $is_temporary, 'columns' ); $columns = $this->execute_sqlite_query( ' - SELECT column_name, is_nullable, data_type, column_default + SELECT LOWER(column_name) AS COLUMN_NAME, is_nullable, data_type, column_default FROM ' . $this->quote_sqlite_identifier( $columns_table ) . ' WHERE table_schema = ? AND table_name = ? @@ -3062,7 +3082,7 @@ private function translate_update_list_in_non_strict_mode( string $table_name, W // Get column info. $column_name = $this->unquote_sqlite_identifier( $this->translate( $column_ref ) ); - $column_info = $column_map[ $column_name ]; + $column_info = $column_map[ strtolower( $column_name ) ]; $data_type = $column_info['DATA_TYPE']; $is_nullable = 'YES' === $column_info['IS_NULLABLE']; $default = $column_info['COLUMN_DEFAULT']; 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 2b92025..7d2fd21 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 @@ -18,120 +18,105 @@ class WP_SQLite_Information_Schema_Builder { * introspection and representation, currently covering the following tables: * * - TABLES - * - VIEWS * - COLUMNS * - STATISTICS (indexes) - * - TABLE_CONSTRAINTS (PK, UNIQUE, FK) + * - TABLE_CONSTRAINTS + * + * TODO (not yet implemented): + * - VIEWS * - CHECK_CONSTRAINTS * - KEY_COLUMN_USAGE (foreign keys) * - REFERENTIAL_CONSTRAINTS (foreign keys) * - TRIGGERS */ - const CREATE_INFORMATION_SCHEMA_QUERIES = array( - // TABLES - "CREATE TABLE IF NOT EXISTS tables ( -- '' is a placeholder replaced at runtime - 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 + const INFORMATION_SCHEMA_TABLE_DEFINITIONS = array( + // INFORMATION_SCHEMA.TABLES + 'tables' => " + TABLE_CATALOG TEXT NOT NULL DEFAULT 'def' COLLATE NOCASE, -- always 'def' + TABLE_SCHEMA TEXT NOT NULL COLLATE NOCASE, -- database name + TABLE_NAME TEXT NOT NULL COLLATE NOCASE, -- table name + TABLE_TYPE TEXT NOT NULL COLLATE BINARY, -- 'BASE TABLE', 'VIEW', or 'SYSTEM VIEW' + ENGINE TEXT NOT NULL COLLATE NOCASE, -- storage engine + VERSION INTEGER NOT NULL DEFAULT 10, -- unused, in MySQL 8 hardcoded to 10 + ROW_FORMAT TEXT NOT NULL COLLATE BINARY, -- 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 NOT NULL DEFAULT '', -- extra CREATE TABLE options - TABLE_COMMENT TEXT NOT NULL DEFAULT '', -- comment + UPDATE_TIME TEXT, -- table update time + CHECK_TIME TEXT, -- not implemented + TABLE_COLLATION TEXT NOT NULL COLLATE NOCASE, -- table collation + CHECKSUM INTEGER, -- not implemented + CREATE_OPTIONS TEXT NOT NULL DEFAULT '' COLLATE NOCASE, -- extra CREATE TABLE options + TABLE_COMMENT TEXT NOT NULL DEFAULT '' COLLATE NOCASE, -- comment PRIMARY KEY (TABLE_SCHEMA, TABLE_NAME) - ) STRICT", - - // COLUMNS - "CREATE TABLE IF NOT EXISTS columns ( -- '' is a placeholder replaced at runtime - 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 + ", + + // INFORMATION_SCHEMA.COLUMNS + 'columns' => " + TABLE_CATALOG TEXT NOT NULL DEFAULT 'def' COLLATE NOCASE, -- always 'def' + TABLE_SCHEMA TEXT NOT NULL COLLATE NOCASE, -- database name + TABLE_NAME TEXT NOT NULL COLLATE NOCASE, -- table name + COLUMN_NAME TEXT NOT NULL COLLATE NOCASE, -- column name + ORDINAL_POSITION INTEGER NOT NULL, -- column position + COLUMN_DEFAULT TEXT COLLATE BINARY, -- default value, NULL for both NULL and none + IS_NULLABLE TEXT NOT NULL COLLATE NOCASE, -- 'YES' or 'NO' + DATA_TYPE TEXT NOT NULL COLLATE BINARY, -- 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 COLLATE NOCASE, -- charset for string columns + COLLATION_NAME TEXT COLLATE NOCASE, -- collation for string columns + COLUMN_TYPE TEXT NOT NULL COLLATE BINARY, -- full data type (with length, precision, etc.) + COLUMN_KEY TEXT NOT NULL DEFAULT '' COLLATE BINARY, -- if column is indexed ('', 'PRI', 'UNI', 'MUL') + EXTRA TEXT NOT NULL DEFAULT '' COLLATE NOCASE, -- AUTO_INCREMENT, VIRTUAL, STORED, etc. + PRIVILEGES TEXT NOT NULL COLLATE NOCASE, -- not implemented + COLUMN_COMMENT TEXT NOT NULL DEFAULT '' COLLATE BINARY, -- comment + GENERATION_EXPRESSION TEXT NOT NULL DEFAULT '' COLLATE BINARY, -- expression for generated columns + SRS_ID INTEGER, -- not implemented PRIMARY KEY (TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME) - ) STRICT", - - // VIEWS - // @TODO: Implement. - "CREATE TABLE IF NOT EXISTS views ( -- '' is a placeholder replaced at runtime - 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, - PRIMARY KEY (TABLE_SCHEMA, TABLE_NAME) - ) STRICT", - - // STATISTICS (indexes) - "CREATE TABLE IF NOT EXISTS statistics ( -- '' is a placeholder replaced at runtime - 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 + ", + + // INFORMATION_SCHEMA.STATISTICS (indexes) + 'statistics' => " + TABLE_CATALOG TEXT NOT NULL DEFAULT 'def' COLLATE NOCASE, -- always 'def' + TABLE_SCHEMA TEXT NOT NULL COLLATE NOCASE, -- database name + TABLE_NAME TEXT NOT NULL COLLATE NOCASE, -- table name + NON_UNIQUE INTEGER NOT NULL, -- 0 for unique indexes, 1 otherwise + INDEX_SCHEMA TEXT NOT NULL COLLATE NOCASE, -- index database name + INDEX_NAME TEXT NOT NULL COLLATE NOCASE, -- index name, for PKs always 'PRIMARY' + SEQ_IN_INDEX INTEGER NOT NULL, -- column position in index (from 1) + COLUMN_NAME TEXT COLLATE NOCASE, -- column name (NULL for functional indexes) + COLLATION TEXT COLLATE NOCASE, -- 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 COLLATE NOCASE, -- 'YES' if column can contain NULL, '' otherwise + INDEX_TYPE TEXT NOT NULL COLLATE BINARY, -- 'BTREE', 'FULLTEXT', 'SPATIAL' + COMMENT TEXT NOT NULL DEFAULT '' COLLATE NOCASE, -- not implemented + INDEX_COMMENT TEXT NOT NULL DEFAULT '' COLLATE BINARY, -- index comment + IS_VISIBLE TEXT NOT NULL DEFAULT 'YES' COLLATE NOCASE, -- 'NO' if column is hidden, 'YES' otherwise + EXPRESSION TEXT COLLATE BINARY, -- expression for functional indexes PRIMARY KEY (TABLE_SCHEMA, TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX), UNIQUE (INDEX_SCHEMA, TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX) - ) STRICT", - - // TABLE_CONSTRAINTS - // @TODO: Implement. Could this be just a view? - "CREATE TABLE IF NOT EXISTS table_constraints ( -- '' is a placeholder replaced at runtime - 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, + ", + + // INFORMATION_SCHEMA.TABLE_CONSTRAINTS + 'table_constraints' => " + CONSTRAINT_CATALOG TEXT NOT NULL DEFAULT 'def' COLLATE NOCASE, -- always 'def' + CONSTRAINT_SCHEMA TEXT NOT NULL COLLATE NOCASE, -- constraint database name + CONSTRAINT_NAME TEXT NOT NULL COLLATE NOCASE, -- constraint name + TABLE_SCHEMA TEXT NOT NULL COLLATE NOCASE, -- table database name + TABLE_NAME TEXT NOT NULL COLLATE NOCASE, -- table name + CONSTRAINT_TYPE TEXT NOT NULL COLLATE BINARY, -- constraint type ('PRIMARY KEY', 'UNIQUE', 'FOREIGN KEY', 'CHECK') + ENFORCED TEXT NOT NULL DEFAULT 'YES' COLLATE BINARY, -- 'YES' if constraint is enforced, 'NO' otherwise -- Constraint names are unique per type in each table. -- A MySQL table can have a PRIMARY KEY, UNIQUE, FOREIGN KEY, and CHECK @@ -139,79 +124,7 @@ class WP_SQLite_Information_Schema_Builder { -- CHECK and FOREIGN KEY constraint names must also be unique per schema. PRIMARY KEY (TABLE_SCHEMA, TABLE_NAME, CONSTRAINT_TYPE, CONSTRAINT_NAME), UNIQUE (CONSTRAINT_SCHEMA, TABLE_NAME, CONSTRAINT_TYPE, CONSTRAINT_NAME) - ) STRICT", - - // CHECK_CONSTRAINTS - // @TODO: Implement. - "CREATE TABLE IF NOT EXISTS check_constraints ( -- '' is a placeholder replaced at runtime - CONSTRAINT_CATALOG TEXT NOT NULL, - CONSTRAINT_SCHEMA TEXT NOT NULL, - CONSTRAINT_NAME TEXT NOT NULL, - CHECK_CLAUSE TEXT NOT NULL, - PRIMARY KEY (CONSTRAINT_SCHEMA, CONSTRAINT_NAME) -- CHECK constraints must be unique per schema - ) STRICT", - - // KEY_COLUMN_USAGE - // @TODO: Implement. - "CREATE TABLE IF NOT EXISTS key_column_usage ( -- '' is a placeholder replaced at runtime - 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 - -- TODO: PRIMARY/UNIQUE keys, if needed. - ) STRICT", - - // REFERENTIAL_CONSTRAINTS - // @TODO: Implement. - "CREATE TABLE IF NOT EXISTS referential_constraints ( -- '' is a placeholder replaced at runtime - 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, - PRIMARY KEY (CONSTRAINT_SCHEMA, CONSTRAINT_NAME) -- FOREIGN KEY constraints must be unique per schema - ) STRICT", - - // TRIGGERS - // @TODO: Implement. - "CREATE TABLE IF NOT EXISTS triggers ( -- '' is a placeholder replaced at runtime - 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, - PRIMARY KEY (TRIGGER_SCHEMA, TRIGGER_NAME) - ) STRICT", + ", ); /** @@ -428,8 +341,15 @@ public function temporary_table_exists( string $table_name ): bool { * database. Tables that are missing will be created. */ public function ensure_information_schema_tables(): void { - foreach ( self::CREATE_INFORMATION_SCHEMA_QUERIES as $query ) { - $this->connection->query( str_replace( '', $this->table_prefix, $query ) ); + foreach ( self::INFORMATION_SCHEMA_TABLE_DEFINITIONS as $table_name => $table_body ) { + $this->connection->query( + sprintf( + 'CREATE TABLE IF NOT EXISTS %s%s (%s) STRICT', + $this->table_prefix, + $table_name, + $table_body + ) + ); } } @@ -438,9 +358,15 @@ public function ensure_information_schema_tables(): void { * the SQLite database. Tables that are missing will be created. */ public function ensure_temporary_information_schema_tables(): void { - foreach ( self::CREATE_INFORMATION_SCHEMA_QUERIES as $query ) { - $query = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $query ); - $this->connection->query( str_replace( '', $this->temporary_table_prefix, $query ) ); + foreach ( self::INFORMATION_SCHEMA_TABLE_DEFINITIONS as $table_name => $table_body ) { + $this->connection->query( + sprintf( + 'CREATE TEMPORARY TABLE IF NOT EXISTS %s%s (%s) STRICT', + $this->temporary_table_prefix, + $table_name, + $table_body + ) + ); } $this->temporary_information_schema_exists = true; } @@ -535,17 +461,32 @@ public function record_create_table( WP_Parser_Node $node ): void { throw $e; } - // Inline column constraint. - $column_constraint_data = $this->extract_column_constraint_data( + // Inline column constraints and indexes. + $index_data = $this->extract_column_statistics_data( $table_name, $column_name, $column_node, 'YES' === $column_data['is_nullable'] ); - if ( null !== $column_constraint_data ) { + + if ( null !== $index_data ) { $this->insert_values( $this->get_table_name( $table_is_temporary, 'statistics' ), - $column_constraint_data + $index_data + ); + } + + // Save constraint data. + $constraint_data = $this->extract_table_constraint_data( + $column_node, + $table_name, + $index_data['index_name'] ?? null + ); + + if ( null !== $constraint_data ) { + $this->insert_values( + $this->get_table_name( $table_is_temporary, 'table_constraints' ), + $constraint_data ); } @@ -686,6 +627,13 @@ public function record_drop_table( WP_Parser_Node $node ): void { 'table_name' => $table_name, ) ); + $this->delete_values( + $this->get_table_name( $table_is_temporary, 'table_constraints' ), + array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + ) + ); } // @TODO: RESTRICT vs. CASCADE @@ -729,11 +677,23 @@ private function record_add_column( throw $e; } - $column_constraint_data = $this->extract_column_constraint_data( $table_name, $column_name, $node, true ); - if ( null !== $column_constraint_data ) { + $index_data = $this->extract_column_statistics_data( $table_name, $column_name, $node, true ); + if ( null !== $index_data ) { $this->insert_values( $this->get_table_name( $table_is_temporary, 'statistics' ), - $column_constraint_data + $index_data + ); + } + + $constraint_data = $this->extract_table_constraint_data( + $node, + $table_name, + $index_data['index_name'] ?? null + ); + if ( null !== $constraint_data ) { + $this->insert_values( + $this->get_table_name( $table_is_temporary, 'table_constraints' ), + $constraint_data ); } } @@ -783,19 +743,31 @@ private function record_change_column( // 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( + $index_data = $this->extract_column_statistics_data( $table_name, $new_column_name, $node, 'YES' === $column_data['is_nullable'] ); - if ( null !== $column_constraint_data ) { + if ( null !== $index_data ) { $this->insert_values( $this->get_table_name( $table_is_temporary, 'statistics' ), - $column_constraint_data + $index_data ); $this->sync_column_key_info( $table_is_temporary, $table_name ); } + + $constraint_data = $this->extract_table_constraint_data( + $node, + $table_name, + $index_data['index_name'] ?? null + ); + if ( null !== $constraint_data ) { + $this->insert_values( + $this->get_table_name( $table_is_temporary, 'table_constraints' ), + $constraint_data + ); + } } /** @@ -827,6 +799,7 @@ private function record_drop_column( string $table_name, string $column_name ): void { + // Delete the column record from the columns table. $this->delete_values( $this->get_table_name( $table_is_temporary, 'columns' ), array( @@ -836,7 +809,18 @@ private function record_drop_column( ) ); - /** + /* + * When a column is dropped, we need to reflect the effects of the change + * on the existing indexes and constraints that the column was part of. + * + * This means: + * + * 1. Remove the column records from the statistics table. + * 2. Renumber SEQ_IN_INDEX values in the statistics table so that + * there are no sequence gaps caused by the removed column. + * 3. Recompute column key information in the statistics table. + * 4. Delete the table constraint records for no longer existing indexes. + * * From MySQL documentation: * * If columns are dropped from a table, the columns are also removed @@ -849,8 +833,17 @@ private function record_drop_column( * See: * - https://dev.mysql.com/doc/refman/8.4/en/alter-table.html */ + $statistics_table = $this->get_table_name( $table_is_temporary, 'statistics' ); + $constraints_table = $this->get_table_name( $table_is_temporary, 'table_constraints' ); + + /* + * 1. Delete the column records from the statistics table. + * + * In MySQL, when a column is dropped, it is removed from all indexes + * that it was part of. An index is dropped when it has no more columns. + */ $this->delete_values( - $this->get_table_name( $table_is_temporary, 'statistics' ), + $statistics_table, array( 'table_schema' => $this->db_name, 'table_name' => $table_name, @@ -858,9 +851,62 @@ private function record_drop_column( ) ); - // @TODO: Renumber SEQ_IN_INDEX values. + /* + * 2. Renumber SEQ_IN_INDEX values in the statistics table. + * + * When a column is removed from a multi-column index, it can leave a gap + * in the numeric sequence of SEQ_IN_INDEX values in the statistics table. + */ + $this->connection->query( + sprintf( + 'UPDATE %s AS statistics + SET seq_in_index = renumbered.seq_in_index + FROM ( + SELECT + rowid, + row_number() OVER (PARTITION BY index_name ORDER BY seq_in_index) AS seq_in_index + FROM %s + WHERE table_schema = ? + AND table_name = ? + ) AS renumbered + WHERE statistics.rowid = renumbered.rowid + AND statistics.seq_in_index != renumbered.seq_in_index', + $this->connection->quote_identifier( $statistics_table ), + $this->connection->quote_identifier( $statistics_table ) + ), + array( $this->db_name, $table_name ) + ); + /* + * 3. Recompute column key data in the statistics table. + * + * When a column is removed from a multi-column index, it can cause the + * value of COLUMN_KEY in the statistics for other columns to change. + */ $this->sync_column_key_info( $table_is_temporary, $table_name ); + + /* + * 4. Delete the table constraint records for no longer existing indexes. + * + * If there are no more columns left in an index the column was part of, + * we need to make sure that the associated table constraint records are + * deleted as well. Therefore, remove all index-specific table constraint + * records that have no index data associated with them for a given table. + */ + $this->connection->query( + sprintf( + "DELETE FROM %s + WHERE table_schema = ? + AND table_name = ? + AND constraint_type IN ('PRIMARY KEY', 'UNIQUE') + AND constraint_name NOT IN ( + SELECT DISTINCT index_name FROM %s WHERE table_schema = ? AND table_name = ? + )", + $this->connection->quote_identifier( $constraints_table ), + $this->connection->quote_identifier( $statistics_table ) + ), + array( $this->db_name, $table_name, $this->db_name, $table_name ) + ); } /** @@ -875,6 +921,7 @@ private function record_drop_index( string $table_name, string $index_name ): void { + // Delete index data. $this->delete_values( $this->get_table_name( $table_is_temporary, 'statistics' ), array( @@ -883,6 +930,33 @@ private function record_drop_index( 'index_name' => $index_name, ) ); + + /* + * Delete associated table constraint data. + * + * A table constraint record is saved for PRIMARY KEY and UNIQUE indexes. + * We don't need to read the schema to get the constraint type, because: + * + * 1. In MySQL, all primary keys are named "PRIMARY", and no other + * indexes can be named so. This way we can identify primary keys. + * 2. In MySQL, all indexes in a table must have distinct names, no + * matter the index type. Therefore, if a table constraint record + * exists for a given index name, we know it is a unique index. + */ + $constraint_type = + strtoupper( $index_name ) === 'PRIMARY' ? 'PRIMARY KEY' : 'UNIQUE'; + + $this->delete_values( + $this->get_table_name( $table_is_temporary, 'table_constraints' ), + array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + 'constraint_name' => $index_name, + 'constraint_type' => $constraint_type, + ) + ); + + // Sync column info from constraint data. $this->sync_column_key_info( $table_is_temporary, $table_name ); } @@ -960,7 +1034,7 @@ private function record_add_constraint( $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_name = $this->get_index_name( $node, $table_name ); $index_type = $this->get_index_type( $node, $keyword, $has_spatial_column ); $index_comment = $this->get_index_comment( $node ); $seq_in_index = 1; @@ -982,7 +1056,7 @@ private function record_add_constraint( $has_spatial_column ); - $column_constraint_data = array( + $index_data = array( 'table_schema' => $this->db_name, 'table_name' => $table_name, 'non_unique' => $non_unique, @@ -1005,7 +1079,7 @@ private function record_add_constraint( try { $this->insert_values( $this->get_table_name( $table_is_temporary, 'statistics' ), - $column_constraint_data + $index_data ); } catch ( PDOException $e ) { if ( '23000' === $e->getCode() ) { @@ -1017,6 +1091,20 @@ private function record_add_constraint( $seq_in_index += 1; } + // Save table constraint data. + $constraint_data = $this->extract_table_constraint_data( + $node, + $table_name, + $index_name + ); + + if ( null !== $constraint_data ) { + $this->insert_values( + $this->get_table_name( $table_is_temporary, 'table_constraints' ), + $constraint_data + ); + } + $this->sync_column_key_info( $table_is_temporary, $table_name ); } @@ -1075,9 +1163,14 @@ private function extract_column_data( string $table_name, string $column_name, W * @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. + * @return array|null Column statistics data for the information schema. */ - private function extract_column_constraint_data( string $table_name, string $column_name, WP_Parser_Node $node, bool $nullable ): ?array { + private function extract_column_statistics_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 ); $has_inline_unique_key = null !== $node->get_first_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ); @@ -1106,6 +1199,35 @@ private function extract_column_constraint_data( string $table_name, string $col return null; } + /** + * Extract table constraint data from the "tableConstraintDef" or "columnDefinition" AST node. + * + * @param WP_Parser_Node $node The "tableConstraintDef" or "columnDefinition" AST node. + * @param string $table_name The table name. + * @param string $column_name The column name. + * @return array Table constraint data for the information schema. + */ + public function extract_table_constraint_data( + WP_Parser_Node $node, + string $table_name, + ?string $index_name = null + ): ?array { + $type = $this->get_table_constraint_type( $node ); + if ( null === $type ) { + return null; + } + + // Index name always takes precedence over constraint name. + $name = $index_name ?? $this->get_table_constraint_name( $node ); + return array( + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + 'constraint_schema' => $this->db_name, + 'constraint_name' => $name, + 'constraint_type' => $type, + ); + } + /** * Update column info from constraint data in the statistics table. * @@ -1128,31 +1250,29 @@ private function sync_column_key_info( bool $table_is_temporary, string $table_n $columns_table_name = $this->get_table_name( $table_is_temporary, 'columns' ); $statistics_table_name = $this->get_table_name( $table_is_temporary, 'statistics' ); $this->connection->query( - " - WITH s AS ( + ' + UPDATE ' . $this->connection->quote_identifier( $columns_table_name ) . " AS c + SET (column_key, is_nullable) = ( 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' + WHEN MAX(s.index_name = 'PRIMARY') THEN 'PRI' + WHEN MAX(s.non_unique = 0 AND s.seq_in_index = 1) THEN 'UNI' + WHEN MAX(s.seq_in_index = 1) THEN 'MUL' ELSE '' - END AS column_key - FROM " . $this->connection->quote_identifier( $statistics_table_name ) . ' - WHERE table_schema = ? - AND table_name = ? - GROUP BY column_name + END, + CASE + WHEN MAX(s.index_name = 'PRIMARY') THEN 'NO' + ELSE c.is_nullable + END + FROM " . $this->connection->quote_identifier( $statistics_table_name ) . ' AS s + WHERE s.table_schema = c.table_schema + AND s.table_name = c.table_name + AND s.column_name = c.column_name ) - UPDATE ' . $this->connection->quote_identifier( $columns_table_name ) . " 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 ) + ', + array( $this->db_name, $table_name ) ); } @@ -1738,33 +1858,120 @@ private function get_column_generation_expression( WP_Parser_Node $node ): strin return ''; } + /** + * Extract table constraint name from the "tableConstraintDef" or "columnDefinition" AST node. + * + * @param WP_Parser_Node $node The "tableConstraintDef" or "columnDefinition" AST node. + * @return string|null The table constraint name. + */ + public function get_table_constraint_name( WP_Parser_Node $node ): ?string { + $name_node = $node->get_first_child_node( 'constraintName' ); + if ( null !== $name_node ) { + return $this->get_value( $name_node ); + } + + // TODO: Handle CHECK/FOREIGN KEY/UNIQUE constraints. + return null; + } + + /** + * Extract table constraint type from the "tableConstraintDef" or "columnDefinition" AST node. + * + * @param WP_Parser_Node $node The "tableConstraintDef" or "columnDefinition" AST node. + * @return string|null The table constraint type as stored in information schema. + */ + private function get_table_constraint_type( WP_Parser_Node $node ): ?string { + if ( $node->get_first_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) { + return 'PRIMARY KEY'; + } + if ( $node->get_first_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ) ) { + return 'UNIQUE'; + } + if ( $node->get_first_descendant_token( WP_MySQL_Lexer::FOREIGN_SYMBOL ) ) { + return 'FOREIGN KEY'; + } + if ( $node->get_first_descendant_token( WP_MySQL_Lexer::CHECK_SYMBOL ) ) { + return 'CHECK'; + } + return null; + } + /** * 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. + * @param WP_Parser_Node $node The "tableConstraintDef" AST node. + * @param string $table_name The table name. + * @return string The index name as stored in information schema. */ - private function get_index_name( WP_Parser_Node $node ): string { + private function get_index_name( WP_Parser_Node $node, string $table_name ): string { if ( $node->get_first_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) { return 'PRIMARY'; } + /* + * Get index name. + * + * When both index and constraint name are defined, the index name will + * be used. E.g., in "CONSTRAINT c UNIQUE u (id)", the name will be "u". + */ $name_node = $node->get_first_descendant_node( 'indexName' ); + if ( null === $name_node && $node->has_child_node( 'constraintName' ) ) { + $name_node = $node + ->get_first_child_node( 'constraintName' ) + ->get_first_child_node( 'identifier' ); + } + 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 any part is an expression, the name will be "functional_index". * If the name is already used, we need to append a number. */ $subnode = $node->get_first_child_node( 'keyListVariants' )->get_first_child_node(); - if ( 'exprWithParentheses' === $subnode->rule_name ) { + if ( null !== $subnode->get_first_descendant_node( 'exprWithParentheses' ) ) { $name = 'functional_index'; } else { $name = $this->get_value( $subnode->get_first_descendant_node( 'identifier' ) ); } - // @TODO: Check if the name is already used. - return $name; + // Check if the name is already used. + $existing_indices = $this->connection->query( + sprintf( + "SELECT DISTINCT index_name + FROM %s + WHERE table_schema = ? + AND table_name = ? + AND (index_name = ? OR index_name LIKE ? ESCAPE '\\')", + $this->connection->quote_identifier( + $this->get_table_name( + $this->temporary_table_exists( $table_name ), + 'statistics' + ) + ) + ), + array( + $this->db_name, + $table_name, + $name, + str_replace( array( '_', '%' ), array( '\\_', '\\%' ), $name ) . '\\_%', + ) + )->fetchAll( + PDO::FETCH_COLUMN // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO + ); + + // The name is not used - we can use it as-is. + if ( count( $existing_indices ) === 0 ) { + return $name; + } + + // The name is used - find the first unused name. + $new_name = $name; + $suffix = 2; + while ( in_array( $new_name, $existing_indices, true ) ) { + $new_name = $name . '_' . $suffix; + $suffix += 1; + } + return $new_name; } return $this->get_value( $name_node ); } @@ -1943,6 +2150,19 @@ private function get_value( WP_Parser_Node $node ): string { foreach ( $node->get_children() as $child ) { if ( $child instanceof WP_Parser_Node ) { $value = $this->get_value( $child ); + + /* + * At the moment, we only support ASCII bytes in all identifiers. + * This is because SQLite doesn't support case-insensitive Unicode + * character matching: https://sqlite.org/faq.html#q18 + */ + if ( 'pureIdentifier' === $child->rule_name ) { + for ( $i = 0; $i < strlen( $value ); $i++ ) { + if ( ord( $value[ $i ] ) > 127 ) { + throw new Exception( 'The SQLite driver only supports ASCII characters in identifiers.' ); + } + } + } } else { $value = $child->get_value(); }