From fea3e193cc631c6d466d2d54d4b98c0fde45adbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jakes=CC=8C?= Date: Fri, 23 Oct 2026 16:48:52 +0200 Subject: [PATCH] Preserve legacy string values when rebuilding tables Cast copied values to TEXT or BLOB using the destination column types. Preserve bytes and NULL values when rebuilding legacy tables or changing between text and binary types. Cover string types, renamed columns, temporary tables, rollback, and temporal-default upgrades with regression tests. Fixes https://github.com/WordPress/sqlite-database-integration/issues/546 --- .../src/sqlite/class-wp-mysql-on-sqlite.php | 38 ++- ...WP_MySQL_On_SQLite_Table_Rebuild_Tests.php | 291 ++++++++++++++++++ .../WP_MySQL_On_SQLite_Translation_Tests.php | 9 + 3 files changed, 325 insertions(+), 13 deletions(-) create mode 100644 packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Table_Rebuild_Tests.php diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php index eee3f050b..659a6c7a9 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php @@ -5727,23 +5727,38 @@ private function recreate_table_from_information_schema( string $table_name, ?array $column_map = null ): void { + $columns_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'columns' ); + $column_types = $this->execute_sqlite_query( + sprintf( + 'SELECT COLUMN_NAME, DATA_TYPE FROM %s WHERE table_schema = ? AND table_name = ?', + $this->quote_sqlite_identifier( $columns_table ) + ), + array( $this->get_saved_db_name(), $table_name ) + )->fetchAll( PDO::FETCH_KEY_PAIR ); + if ( null === $column_map ) { - $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 = ?', - $this->quote_sqlite_identifier( $columns_table ) - ), - array( $this->get_saved_db_name(), $table_name ) - )->fetchAll( PDO::FETCH_COLUMN ); - $column_map = array_combine( $column_names, $column_names ); + $column_names = array_keys( $column_types ); + $column_map = array_combine( $column_names, $column_names ); } + $column_types = array_change_key_case( $column_types, CASE_LOWER ); // Preserve ROWIDs. // This also addresses a special case when all original columns are dropped // and there is nothing to copy. We'll always have at least the ROWID column. $column_map = array( 'rowid' => 'rowid' ) + $column_map; + $select_list = array(); + foreach ( $column_map as $old_name => $new_name ) { + $value = $this->quote_sqlite_identifier( $old_name ); + $mysql_type = $column_types[ strtolower( $new_name ) ] ?? ''; + $sqlite_type = self::DATA_TYPE_STRING_MAP[ $mysql_type ] ?? null; + // Legacy tables may store text and binary values with a different storage class. + if ( 'TEXT' === $sqlite_type || 'BLOB' === $sqlite_type ) { + $value = sprintf( 'CAST(%s AS %s)', $value, $sqlite_type ); + } + $select_list[] = $value; + } + /* * See: * https://www.sqlite.org/lang_altertable.html#making_other_kinds_of_table_schema_changes @@ -5771,10 +5786,7 @@ private function recreate_table_from_information_schema( ', ', array_map( array( $this, 'quote_sqlite_identifier' ), $column_map ) ), - implode( - ', ', - array_map( array( $this, 'quote_sqlite_identifier' ), array_keys( $column_map ) ) - ), + implode( ', ', $select_list ), $quoted_table_name ) ); diff --git a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Table_Rebuild_Tests.php b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Table_Rebuild_Tests.php new file mode 100644 index 000000000..247296300 --- /dev/null +++ b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Table_Rebuild_Tests.php @@ -0,0 +1,291 @@ += 80400 ? Pdo\Sqlite::class : PDO::class; + $this->sqlite = new $pdo_class( 'sqlite::memory:' ); + $this->engine = new WP_MySQL_On_SQLite( + 'mysql-on-sqlite:dbname=wp', + null, + null, + array( 'sqlite_pdo' => $this->sqlite ) + ); + } + + /** + * @dataProvider stringTableRebuilds + */ + public function testRebuildPreservesLegacyStringValues( string $type, string $sqlite_type, string $query, string $column ): void { + $this->createLegacyStringTable( $type, $sqlite_type ); + $before = $this->getTableValues( 'Pay`load' ); + + $result = $this->engine->query( sprintf( $query, $type ) ); + if ( $result->columnCount() > 0 ) { + // Administration statements report errors in their result rows. + $this->assertSame( array( 'status' ), array_column( $result->fetchAll( PDO::FETCH_ASSOC ), 'Msg_type' ) ); + } + + $this->assertSame( $before, $this->getTableValues( $column ) ); + $quoted_column = $this->engine->get_connection()->quote_identifier( $column ); + $this->assertSame( + array( strtolower( $sqlite_type ) ), + $this->sqlite->query( + "SELECT DISTINCT typeof($quoted_column) FROM t WHERE $quoted_column IS NOT NULL" + )->fetchAll( PDO::FETCH_COLUMN ) + ); + } + + public static function stringTableRebuilds(): array { + $queries = array( + 'add' => array( 'ALTER TABLE t ADD COLUMN added TEXT', 'Pay`load' ), + 'modify' => array( 'ALTER TABLE t MODIFY COLUMN `pay``LOAD` %s', 'pay`LOAD' ), + 'change' => array( 'ALTER TABLE t CHANGE COLUMN `pay``LOAD` `New``Payload` %s', 'New`Payload' ), + 'case rename' => array( 'ALTER TABLE t CHANGE COLUMN `pay``LOAD` `PAY``LOAD` %s', 'PAY`LOAD' ), + 'optimize' => array( 'OPTIMIZE TABLE t', 'Pay`load' ), + 'repair' => array( 'REPAIR TABLE t', 'Pay`load' ), + 'drop primary' => array( 'DROP INDEX `PRIMARY` ON t', 'Pay`load' ), + ); + $types = array( + 'BLOB' => array( 'BINARY(32)', 'VARBINARY(32)', 'TINYBLOB', 'BLOB', 'MEDIUMBLOB', 'LONGBLOB' ), + 'TEXT' => array( 'CHAR(32)', 'VARCHAR(32)', 'NCHAR(32)', 'NVARCHAR(32)', 'TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT' ), + ); + $cases = array(); + foreach ( $types as $sqlite_type => $mysql_types ) { + foreach ( $mysql_types as $type ) { + foreach ( $queries as $name => $query ) { + $cases[ "$type $name" ] = array( $type, $sqlite_type, $query[0], $query[1] ); + } + } + } + return $cases; + } + + /** + * @dataProvider stringColumnTypeChanges + */ + public function testRebuildUsesChangedColumnType( string $old_type, string $new_type, string $storage_class ): void { + $this->engine->exec( "CREATE TABLE t (payload $old_type)" ); + $this->engine->exec( "INSERT INTO t VALUES ('000123'), ('abc'), (NULL)" ); + $this->engine->exec( "ALTER TABLE t CHANGE COLUMN payload renamed $new_type" ); + $this->assertSame( + array( + array( + 'hex' => '303030313233', + 'type' => $storage_class, + ), + array( + 'hex' => '616263', + 'type' => $storage_class, + ), + array( + 'hex' => '', + 'type' => 'null', + ), + ), + $this->sqlite->query( 'SELECT hex(renamed) AS hex, typeof(renamed) AS type FROM t ORDER BY rowid' )->fetchAll( PDO::FETCH_ASSOC ) + ); + } + + public static function stringColumnTypeChanges(): array { + return array( + 'text to binary' => array( 'TEXT', 'VARBINARY(32)', 'blob' ), + 'blob to text' => array( 'BLOB', 'TEXT', 'text' ), + ); + } + + /** + * @dataProvider textBackedColumnValues + */ + public function testRebuildPreservesTextBackedColumnValues( string $type, string $value ): void { + $this->engine->exec( "CREATE TABLE t (payload $type)" ); + $this->sqlite->exec( 'DROP TABLE t' ); + $this->sqlite->exec( 'CREATE TABLE t (payload TEXT)' ); + $this->sqlite->exec( "INSERT INTO t VALUES (x'" . bin2hex( $value ) . "')" ); + + $this->engine->exec( 'ALTER TABLE t ADD COLUMN note TEXT' ); + + $this->assertSame( $value, $this->sqlite->query( 'SELECT payload FROM t' )->fetchColumn() ); + $this->assertSame( 'text', $this->sqlite->query( 'SELECT typeof(payload) FROM t' )->fetchColumn() ); + } + + public static function textBackedColumnValues(): array { + return array( + 'json' => array( 'JSON', '{"name":"žluťoučký"}' ), + 'enum' => array( "ENUM('abc','def')", 'abc' ), + 'set' => array( "SET('a','b')", 'a,b' ), + 'date' => array( 'DATE', '2024-02-29' ), + 'time' => array( 'TIME(3)', '12:34:56.123' ), + 'datetime' => array( 'DATETIME(3)', '2024-02-29 12:34:56.123' ), + 'timestamp' => array( 'TIMESTAMP(3)', '2024-02-29 12:34:56.123' ), + 'year' => array( 'YEAR', '2024' ), + 'geometry' => array( 'GEOMETRY', 'POINT(1 2)' ), + ); + } + + /** + * @dataProvider stringStorageTypes + */ + public function testRebuildUsesTemporaryColumnMetadata( string $type ): void { + $main_type = 'BLOB' === $type ? 'TEXT' : 'BLOB'; + $this->engine->exec( "CREATE TABLE t (`Pay``load` $main_type)" ); + $this->engine->exec( "INSERT INTO t VALUES ('main table')" ); + $this->createLegacyStringTable( $type, $type, true ); + $before = $this->getTableValues( 'Pay`load' ); + + $this->engine->exec( 'ALTER TABLE t ADD COLUMN added TEXT' ); + + $this->assertSame( $before, $this->getTableValues( 'Pay`load' ) ); + $this->assertSame( strtolower( $type ), $this->sqlite->query( 'SELECT typeof(`Pay``load`) FROM temp.t WHERE id = 3' )->fetchColumn() ); + $this->assertSame( 'main table', $this->sqlite->query( 'SELECT `Pay``load` FROM main.t' )->fetchColumn() ); + $this->assertSame( strtolower( $main_type ), $this->sqlite->query( 'SELECT typeof(`Pay``load`) FROM main.t' )->fetchColumn() ); + } + + /** + * @dataProvider stringStorageTypes + */ + public function testFailedRebuildPreservesLegacyValuesAndSchema( string $type ): void { + $this->createLegacyStringTable( $type, $type ); + $this->sqlite->exec( 'UPDATE t SET note = NULL WHERE id = 3' ); + $before = $this->getTableValues( 'Pay`load' ); + $schema = $this->sqlite->query( "SELECT sql FROM sqlite_master WHERE name = 't'" )->fetchColumn(); + + $exception = null; + try { + $this->engine->exec( 'ALTER TABLE t MODIFY COLUMN note TEXT NOT NULL' ); + } catch ( WP_MySQL_On_SQLite_Exception $e ) { + $exception = $e; + } + $this->assertInstanceOf( WP_MySQL_On_SQLite_Exception::class, $exception ); + $this->assertStringContainsString( 'NOT NULL constraint failed', $exception->getMessage() ); + $this->assertSame( $before, $this->getTableValues( 'Pay`load' ) ); + $this->assertSame( $schema, $this->sqlite->query( "SELECT sql FROM sqlite_master WHERE name = 't'" )->fetchColumn() ); + $this->assertSame( 'text', $this->sqlite->query( 'SELECT typeof(`Pay``load`) FROM t WHERE id = 3' )->fetchColumn() ); + $this->assertSame( + 'YES', + $this->engine->query( "SELECT is_nullable FROM information_schema.columns WHERE table_name = 't' AND column_name = 'note'" )->fetchColumn() + ); + $this->assertSame( 0, (int) $this->sqlite->query( "SELECT COUNT(*) FROM sqlite_master WHERE name GLOB '_wp_sqlite_tmp_*'" )->fetchColumn() ); + + $this->engine->exec( 'ALTER TABLE t ADD COLUMN added TEXT' ); + $this->assertSame( $before, $this->getTableValues( 'Pay`load' ) ); + } + + public static function stringStorageTypes(): array { + return array( + 'blob' => array( 'BLOB' ), + 'text' => array( 'TEXT' ), + ); + } + + /** + * @dataProvider legacyMetadataStates + */ + public function testTemporalDefaultMigrationPreservesLegacyStringValues( bool $has_metadata, string $type ): void { + if ( $has_metadata ) { + $this->engine->exec( "CREATE TABLE t (id INT, payload $type, created_at TIMESTAMP DEFAULT 0)" ); + $this->sqlite->exec( 'DROP TABLE t' ); + } else { + $this->sqlite->exec( + 'CREATE TABLE _mysql_data_types_cache ( + `table` TEXT NOT NULL, + column_or_index TEXT NOT NULL, + mysql_type TEXT NOT NULL, + PRIMARY KEY (`table`, column_or_index) + )' + ); + $this->sqlite->exec( + "INSERT INTO _mysql_data_types_cache VALUES + ('t', 'id', 'int'), ('t', 'payload', '" . strtolower( $type ) . "'), ('t', 'created_at', 'timestamp')" + ); + } + $this->sqlite->exec( "CREATE TABLE t (id INTEGER, payload $type, created_at TEXT DEFAULT 0)" ); + $this->sqlite->exec( "INSERT INTO t (id, payload) VALUES (1, CAST(x'6100FF' AS TEXT)), (2, x'00FF80'), (3, NULL)" ); + $this->sqlite->exec( + sprintf( + "UPDATE %s SET value = '0.0.0' WHERE name = '%s'", + WP_MySQL_On_SQLite::GLOBAL_VARIABLES_TABLE_NAME, + WP_MySQL_On_SQLite::DRIVER_VERSION_VARIABLE_NAME + ) + ); + + $this->engine = new WP_MySQL_On_SQLite( + 'mysql-on-sqlite:dbname=wp', + null, + null, + array( 'sqlite_pdo' => $this->sqlite ) + ); + + $this->assertSame( + array( + array( + 'hex' => '6100FF', + 'type' => strtolower( $type ), + 'created_at' => '0000-00-00 00:00:00', + ), + array( + 'hex' => '00FF80', + 'type' => strtolower( $type ), + 'created_at' => '0000-00-00 00:00:00', + ), + array( + 'hex' => '', + 'type' => 'null', + 'created_at' => '0000-00-00 00:00:00', + ), + ), + $this->sqlite->query( 'SELECT hex(payload) AS hex, typeof(payload) AS type, created_at FROM t ORDER BY id' )->fetchAll( PDO::FETCH_ASSOC ) + ); + $this->assertSame( SQLITE_DRIVER_VERSION, $this->engine->get_saved_driver_version() ); + $this->assertSame( + '0000-00-00 00:00:00', + $this->engine->query( "SELECT column_default FROM information_schema.columns WHERE table_name = 't' AND column_name = 'created_at'" )->fetchColumn() + ); + } + + public static function legacyMetadataStates(): array { + return array( + 'previously upgraded blob' => array( true, 'BLOB' ), + 'previously upgraded text' => array( true, 'TEXT' ), + 'legacy metadata blob' => array( false, 'BLOB' ), + 'legacy metadata text' => array( false, 'TEXT' ), + ); + } + + private function createLegacyStringTable( string $type, string $sqlite_type, bool $temporary = false ): void { + $temporary_keyword = $temporary ? 'TEMPORARY' : ''; + $this->engine->exec( "CREATE $temporary_keyword TABLE t (id INT PRIMARY KEY, `Pay``load` $type, note TEXT)" ); + + // Earlier upgrades retained non-STRICT tables and their original storage classes. + $this->sqlite->exec( 'DROP TABLE t' ); + $this->sqlite->exec( "CREATE $temporary_keyword TABLE t (id INT PRIMARY KEY, `Pay``load` $sqlite_type, note TEXT)" ); + $this->sqlite->exec( + "INSERT INTO t (rowid, id, `Pay``load`, note) VALUES + (11, 1, NULL, '000123'), + (12, 2, '', '000123'), + (13, 3, 'abc', '000123'), + (14, 4, '000123', '000123'), + (15, 5, 'žluťoučký', '000123'), + (16, 6, CAST(x'610062' AS TEXT), '000123'), + (17, 7, CAST(x'FF80FE61' AS TEXT), '000123'), + (18, 8, x'00FF80FE61', '000123'), + (19, 9, 123, '000123'), + (20, 10, 1.25, '000123')" + ); + } + + private function getTableValues( string $column ): array { + $quoted_column = $this->engine->get_connection()->quote_identifier( $column ); + return $this->sqlite->query( + "SELECT rowid, id, hex($quoted_column), $quoted_column IS NULL, note, typeof(note) FROM t ORDER BY rowid" + )->fetchAll( PDO::FETCH_NUM ); + } +} diff --git a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Translation_Tests.php b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Translation_Tests.php index a09a0dcdb..941c5c081 100644 --- a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Translation_Tests.php +++ b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Translation_Tests.php @@ -910,6 +910,7 @@ public function testAlterTableAddColumn( bool $stringify_fetches ): void { "SELECT MAX(ordinal_position) FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' 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 ('sqlite_database', 't', 'a', 2, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "SELECT COLUMN_NAME, DATA_TYPE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'sqlite_database' 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", @@ -948,6 +949,7 @@ public function testAlterTableAddColumnWithNotNull(): void { "SELECT MAX(ordinal_position) FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' 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 ('sqlite_database', 't', 'a', 2, null, 'NO', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "SELECT COLUMN_NAME, DATA_TYPE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'sqlite_database' 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", @@ -979,6 +981,7 @@ public function testAlterTableAddColumnWithDefault(): void { "SELECT MAX(ordinal_position) FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' 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 ('sqlite_database', 't', 'a', 2, '0', 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "SELECT COLUMN_NAME, DATA_TYPE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'sqlite_database' 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", @@ -1010,6 +1013,7 @@ public function testAlterTableAddColumnWithNotNullAndDefault(): void { "SELECT MAX(ordinal_position) FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' 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 ('sqlite_database', 't', 'a', 2, '0', 'NO', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "SELECT COLUMN_NAME, DATA_TYPE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'sqlite_database' 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", @@ -1047,6 +1051,7 @@ public function testAlterTableAddMultipleColumns(): void { "SELECT MAX(ordinal_position) FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' 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 ('sqlite_database', 't', 'c', 4, null, 'YES', 'tinyint', null, null, 3, 0, null, null, null, 'tinyint(1)', '', '', 'select,insert,update,references', '', '', null)", + "SELECT COLUMN_NAME, DATA_TYPE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'sqlite_database' 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", @@ -1080,6 +1085,7 @@ public function testAlterTableDropColumn(): void { "WITH renumbered AS ( 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 = 'sqlite_database' AND table_name = 't' ) UPDATE `_wp_sqlite_mysql_information_schema_statistics` AS statistics SET seq_in_index = (SELECT seq_in_index FROM renumbered WHERE rowid = statistics.rowid) WHERE statistics.rowid IN (SELECT rowid FROM renumbered)", "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 = 'sqlite_database' AND c.table_name = 't'", "DELETE FROM `_wp_sqlite_mysql_information_schema_table_constraints` WHERE table_schema = 'sqlite_database' 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 = 'sqlite_database' AND table_name = 't' )", + "SELECT COLUMN_NAME, DATA_TYPE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'sqlite_database' 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", @@ -1117,6 +1123,7 @@ public function testAlterTableDropMultipleColumns(): void { "WITH renumbered AS ( 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 = 'sqlite_database' AND table_name = 't' ) UPDATE `_wp_sqlite_mysql_information_schema_statistics` AS statistics SET seq_in_index = (SELECT seq_in_index FROM renumbered WHERE rowid = statistics.rowid) WHERE statistics.rowid IN (SELECT rowid FROM renumbered)", "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 = 'sqlite_database' AND c.table_name = 't'", "DELETE FROM `_wp_sqlite_mysql_information_schema_table_constraints` WHERE table_schema = 'sqlite_database' 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 = 'sqlite_database' AND table_name = 't' )", + "SELECT COLUMN_NAME, DATA_TYPE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'sqlite_database' 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", @@ -1153,6 +1160,7 @@ public function testAlterTableAddAndDropColumns(): void { "WITH renumbered AS ( 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 = 'sqlite_database' AND table_name = 't' ) UPDATE `_wp_sqlite_mysql_information_schema_statistics` AS statistics SET seq_in_index = (SELECT seq_in_index FROM renumbered WHERE rowid = statistics.rowid) WHERE statistics.rowid IN (SELECT rowid FROM renumbered)", "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 = 'sqlite_database' AND c.table_name = 't'", "DELETE FROM `_wp_sqlite_mysql_information_schema_table_constraints` WHERE table_schema = 'sqlite_database' 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 = 'sqlite_database' AND table_name = 't' )", + "SELECT COLUMN_NAME, DATA_TYPE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'sqlite_database' 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", @@ -1189,6 +1197,7 @@ public function testAlterTableDropAndAddSingleColumn(): void { "SELECT MAX(ordinal_position) FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' 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 ('sqlite_database', 't', 'a', 1, null, 'YES', 'int', null, null, 10, 0, null, null, null, 'int', '', '', 'select,insert,update,references', '', '', null)", + "SELECT COLUMN_NAME, DATA_TYPE FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'sqlite_database' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'sqlite_database' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'sqlite_database' 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",