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..ab466f00d 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 @@ -5779,6 +5779,27 @@ private function recreate_table_from_information_schema( ) ); + // Preserve the AUTO_INCREMENT counter from the original table. + $schema = $table_is_temporary ? 'temp' : 'main'; + $has_sequence_table = (bool) $this->execute_sqlite_query( + sprintf( "SELECT 1 FROM %s.sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'", $schema ) + )->fetchColumn(); + if ( $has_sequence_table ) { + $this->execute_sqlite_query( + sprintf( + 'UPDATE %s.sqlite_sequence + SET seq = MAX(seq, COALESCE(( + SELECT seq FROM %s.sqlite_sequence + WHERE name = ? COLLATE NOCASE -- $table_name: original table + ), 0)) + WHERE name = ? -- $tmp_table_name: update only the replacement\'s row.', + $schema, + $schema + ), + array( $table_name, $tmp_table_name ) + ); + } + // 4. Drop the original table. $this->execute_sqlite_query( sprintf( 'DROP TABLE %s', $quoted_table_name ) ); diff --git a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Metadata_Tests.php b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Metadata_Tests.php index 1366cb840..c3305dec5 100644 --- a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Metadata_Tests.php +++ b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Metadata_Tests.php @@ -929,6 +929,149 @@ public function testAlterTableSetAutoIncrement(): void { $this->assertSame( null, $result[0]->Auto_increment ); } + /** + * @dataProvider tableRebuildsWithAutoIncrement + */ + public function testTableRebuildPreservesAutoIncrement( string $temporary, string $rebuild, array $setup, string $expected_id ): void { + $this->assertQuery( "CREATE $temporary TABLE t (id BIGINT AUTO_INCREMENT PRIMARY KEY, name TEXT)" ); + foreach ( $setup as $query ) { + $this->assertQuery( $query ); + } + $rows = $this->assertQuery( 'SELECT * FROM t ORDER BY id' ); + + $result = $this->assertQuery( $rebuild ); + if ( is_array( $result ) ) { + $this->assertSame( 'OK', $result[0]->Msg_text ); + } + $this->assertEquals( $rows, $this->assertQuery( 'SELECT id, name FROM t ORDER BY id' ) ); + if ( '' === $temporary ) { + $this->assertSame( $expected_id, $this->assertQuery( "SHOW TABLE STATUS LIKE 't'" )[0]->Auto_increment ); + $this->assertSame( + $expected_id, + $this->assertQuery( "SELECT `AUTO_INCREMENT` FROM information_schema.tables WHERE table_name = 't'" )[0]->AUTO_INCREMENT + ); + } + + $this->assertQuery( "INSERT INTO t (name) VALUES ('next')" ); + $this->assertSame( $expected_id, $this->assertQuery( "SELECT id FROM t WHERE name = 'next'" )[0]->id ); + } + + public static function tableRebuildsWithAutoIncrement(): array { + $states = array( + 'deleted highest ID' => array( + array( "INSERT INTO t (name) VALUES ('a'), ('b'), ('c')", 'DELETE FROM t WHERE id = 3' ), + '4', + ), + 'deleted all rows' => array( + array( "INSERT INTO t (name) VALUES ('a'), ('b'), ('c')", 'DELETE FROM t' ), + '4', + ), + 'empty with seed' => array( array( 'ALTER TABLE t AUTO_INCREMENT = 1000' ), '1000' ), + 'empty without seed' => array( array(), '1' ), + 'large sequence' => array( + array( "INSERT INTO t (id, name) VALUES (9007199254740993, 'a')", 'DELETE FROM t' ), + '9007199254740994', + ), + 'updated highest ID' => array( + array( "INSERT INTO t (name) VALUES ('a')", 'UPDATE t SET id = 100' ), + '101', + ), + ); + $cases = array(); + foreach ( array( '', 'TEMPORARY' ) as $temporary ) { + foreach ( array( 'ALTER TABLE t ADD COLUMN note TEXT', 'ALTER TABLE t MODIFY COLUMN name VARCHAR(100)', 'OPTIMIZE TABLE t', 'REPAIR TABLE t' ) as $rebuild ) { + foreach ( $states as $state => list( $setup, $expected_id ) ) { + $cases[ "$temporary $rebuild: $state" ] = array( $temporary, $rebuild, $setup, $expected_id ); + } + } + } + return $cases; + } + + public function testTableRebuildPreservesCreateTableAutoIncrementOption(): void { + $this->assertQuery( 'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT) AUTO_INCREMENT = 1000' ); + $this->assertQuery( 'ALTER TABLE t ADD COLUMN note TEXT' ); + $this->assertQuery( 'OPTIMIZE TABLE t' ); + $this->assertQuery( "INSERT INTO t (name) VALUES ('next')" ); + $this->assertSame( '1000', $this->assertQuery( 'SELECT id FROM t' )[0]->id ); + } + + public function testTableRebuildAppliesExplicitAutoIncrementOption(): void { + $this->assertQuery( 'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT) AUTO_INCREMENT = 1000' ); + $this->assertQuery( 'ALTER TABLE t ADD COLUMN a INT, AUTO_INCREMENT = 500' ); + $this->assertQuery( "INSERT INTO t (name) VALUES ('a')" ); + $this->assertSame( '500', $this->assertQuery( "SELECT id FROM t WHERE name = 'a'" )[0]->id ); + + $this->assertQuery( 'ALTER TABLE t ADD COLUMN b INT, AUTO_INCREMENT = 1' ); + $this->assertQuery( "INSERT INTO t (name) VALUES ('b')" ); + $this->assertSame( '501', $this->assertQuery( "SELECT id FROM t WHERE name = 'b'" )[0]->id ); + + $this->assertQuery( 'ALTER TABLE t ADD COLUMN c INT, AUTO_INCREMENT = 2000' ); + $this->assertQuery( "INSERT INTO t (name) VALUES ('c')" ); + $this->assertSame( '2000', $this->assertQuery( "SELECT id FROM t WHERE name = 'c'" )[0]->id ); + } + + public function testTableRebuildPreservesAutoIncrementWithQuotedNames(): void { + $this->assertQuery( "CREATE TABLE `a``b'c` (`i``d` INT AUTO_INCREMENT PRIMARY KEY, name TEXT) AUTO_INCREMENT = 1000" ); + $this->assertQuery( "ALTER TABLE `a``b'c` CHANGE COLUMN `i``d` `new``id` BIGINT AUTO_INCREMENT" ); + $this->assertQuery( "INSERT INTO `a``b'c` (name) VALUES ('next')" ); + $this->assertSame( '1000', $this->assertQuery( "SELECT `new``id` AS id FROM `a``b'c`" )[0]->id ); + } + + public function testTableRebuildPreservesAutoIncrementWithDifferentTableNameCase(): void { + $this->assertQuery( 'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT) AUTO_INCREMENT = 1000' ); + $this->assertQuery( 'ALTER TABLE T ADD COLUMN note TEXT' ); + $this->assertQuery( "INSERT INTO t (name) VALUES ('next')" ); + $this->assertSame( '1000', $this->assertQuery( 'SELECT id FROM t' )[0]->id ); + } + + public function testTableRebuildKeepsTemporaryAndPersistentSequencesSeparate(): void { + $this->assertQuery( 'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT) AUTO_INCREMENT = 1000' ); + $this->assertQuery( 'CREATE TEMPORARY TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT) AUTO_INCREMENT = 500' ); + $this->assertQuery( 'ALTER TABLE t ADD COLUMN note TEXT' ); + $this->assertQuery( 'OPTIMIZE TABLE t' ); + $this->assertQuery( "INSERT INTO t (name) VALUES ('temporary')" ); + $this->assertSame( '500', $this->assertQuery( 'SELECT id FROM t' )[0]->id ); + + $this->assertQuery( 'DROP TEMPORARY TABLE t' ); + $this->assertQuery( 'ALTER TABLE t ADD COLUMN note TEXT' ); + $this->assertQuery( "INSERT INTO t (name) VALUES ('persistent')" ); + $this->assertSame( '1000', $this->assertQuery( 'SELECT id FROM t' )[0]->id ); + } + + public function testTableRebuildWithoutAutoIncrement(): void { + $this->assertQuery( 'CREATE TABLE t (id INT PRIMARY KEY, name TEXT)' ); + $this->assertQuery( "INSERT INTO t VALUES (1, 'a')" ); + $this->assertQuery( 'ALTER TABLE t ADD COLUMN note TEXT' ); + $this->assertQuery( 'OPTIMIZE TABLE t' ); + $this->assertSame( 'a', $this->assertQuery( 'SELECT name FROM t' )[0]->name ); + $this->assertFalse( $this->sqlite->query( "SELECT 1 FROM sqlite_master WHERE name = 'sqlite_sequence'" )->fetchColumn() ); + + $this->assertQuery( 'ALTER TABLE t MODIFY COLUMN id INT AUTO_INCREMENT' ); + $this->assertQuery( "INSERT INTO t (name) VALUES ('b')" ); + $this->assertSame( '2', $this->assertQuery( "SELECT id FROM t WHERE name = 'b'" )[0]->id ); + + $this->assertQuery( 'ALTER TABLE t AUTO_INCREMENT = 1000' ); + $this->assertQuery( 'ALTER TABLE t MODIFY COLUMN id INT NOT NULL' ); + $this->assertFalse( $this->sqlite->query( "SELECT seq FROM sqlite_sequence WHERE name = 't'" )->fetchColumn() ); + $this->assertSame( null, $this->assertQuery( "SHOW TABLE STATUS LIKE 't'" )[0]->Auto_increment ); + } + + public function testFailedTableRebuildPreservesAutoIncrement(): void { + $this->assertQuery( 'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' ); + $this->assertQuery( "INSERT INTO t (name) VALUES ('a'), ('a'), ('b')" ); + $this->assertQuery( 'DELETE FROM t WHERE id = 3' ); + try { + $this->assertQuery( 'ALTER TABLE t ADD UNIQUE KEY unique_name (name)' ); + $this->fail( 'Adding a unique key on duplicate values must fail.' ); + } catch ( WP_MySQL_On_SQLite_Exception $e ) { + $this->assertSame( '23000', $e->getCode() ); + } + $this->assertQuery( "INSERT INTO t (name) VALUES ('next')" ); + $this->assertSame( '4', $this->assertQuery( "SELECT id FROM t WHERE name = 'next'" )[0]->id ); + $this->assertSame( array( 't' ), $this->sqlite->query( 'SELECT name FROM sqlite_sequence' )->fetchAll( PDO::FETCH_COLUMN ) ); + } + public function testTemporaryTableAutoIncrement(): void { // Persistent and temporary tables must keep independent sequences. diff --git a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php index 64c9ad8c9..03a67acaa 100644 --- a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php +++ b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php @@ -6753,6 +6753,24 @@ public function testConfigurationNormalizesStoredTemporalZeroDefaults(): void { $this->assertEquals( $expected_rows, $this->last_result ); } + public function testConfigurationPreservesAutoIncrementWhenNormalizingTemporalZeroDefaults(): void { + $this->assertQuery( 'CREATE TABLE _tmp_table (id INT AUTO_INCREMENT PRIMARY KEY, created_at DATETIME NOT NULL DEFAULT 0)' ); + $this->assertQuery( 'CREATE TABLE _tmp_empty (id INT AUTO_INCREMENT PRIMARY KEY, created_at DATETIME NOT NULL DEFAULT 0)' ); + $this->sqlite->exec( "UPDATE _wp_sqlite_mysql_information_schema_columns SET column_default = '0' WHERE table_name IN ('_tmp_table', '_tmp_empty') AND column_name = 'created_at'" ); + $this->assertQuery( 'OPTIMIZE TABLE _tmp_table, _tmp_empty' ); + $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (1), (2), (3)' ); + $this->assertQuery( 'DELETE FROM _tmp_table WHERE id = 3' ); + $this->assertQuery( 'ALTER TABLE _tmp_empty AUTO_INCREMENT = 1000' ); + + $this->reconnectAfterDriverUpgrade(); + + $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (NULL)' ); + $this->assertSame( '4', $this->assertQuery( 'SELECT MAX(id) AS id FROM _tmp_table' )[0]->id ); + $this->assertQuery( 'INSERT INTO _tmp_empty (id) VALUES (NULL)' ); + $this->assertSame( '1000', $this->assertQuery( 'SELECT id FROM _tmp_empty' )[0]->id ); + $this->assertSame( '0000-00-00 00:00:00', $this->assertQuery( 'SELECT created_at FROM _tmp_empty' )[0]->created_at ); + } + public function testConfigurationNormalizesStoredTemporalZeroValuesWithoutRunningTriggers(): void { $this->assertQuery( 'CREATE TABLE _tmp_table ( 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..26b29efcd 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 @@ -896,6 +896,7 @@ public function testAlterTableAddColumn( bool $stringify_fetches ): void { 'PRAGMA foreign_keys = OFF', 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER )' . $this->strict_suffix, 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`', + "SELECT 1 FROM main.sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'", 'DROP TABLE `t`', 'ALTER TABLE `` RENAME TO `t`', 'PRAGMA foreign_key_check', @@ -934,6 +935,7 @@ public function testAlterTableAddColumnWithNotNull(): void { 'PRAGMA foreign_keys = OFF', 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER NOT NULL )' . $this->strict_suffix, 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`', + "SELECT 1 FROM main.sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'", 'DROP TABLE `t`', 'ALTER TABLE `` RENAME TO `t`', 'PRAGMA foreign_key_check', @@ -965,6 +967,7 @@ public function testAlterTableAddColumnWithDefault(): void { 'PRAGMA foreign_keys = OFF', 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER DEFAULT \'0\' )' . $this->strict_suffix, 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`', + "SELECT 1 FROM main.sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'", 'DROP TABLE `t`', 'ALTER TABLE `` RENAME TO `t`', 'PRAGMA foreign_key_check', @@ -996,6 +999,7 @@ public function testAlterTableAddColumnWithNotNullAndDefault(): void { 'PRAGMA foreign_keys = OFF', 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER NOT NULL DEFAULT \'0\' )' . $this->strict_suffix, 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`', + "SELECT 1 FROM main.sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'", 'DROP TABLE `t`', 'ALTER TABLE `` RENAME TO `t`', 'PRAGMA foreign_key_check', @@ -1027,6 +1031,7 @@ public function testAlterTableAddMultipleColumns(): void { 'PRAGMA foreign_keys = OFF', 'CREATE TABLE `` ( `id` INTEGER, `a` INTEGER, `b` TEXT COLLATE NOCASE, `c` INTEGER )' . $this->strict_suffix, 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`', + "SELECT 1 FROM main.sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'", 'DROP TABLE `t`', 'ALTER TABLE `` RENAME TO `t`', 'PRAGMA foreign_key_check', @@ -1064,6 +1069,7 @@ public function testAlterTableDropColumn(): void { 'PRAGMA foreign_keys = OFF', 'CREATE TABLE `` ( `id` INTEGER )' . $this->strict_suffix, 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`', + "SELECT 1 FROM main.sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'", 'DROP TABLE `t`', 'ALTER TABLE `` RENAME TO `t`', 'PRAGMA foreign_key_check', @@ -1096,6 +1102,7 @@ public function testAlterTableDropMultipleColumns(): void { 'PRAGMA foreign_keys = OFF', 'CREATE TABLE `` ( `id` INTEGER )' . $this->strict_suffix, 'INSERT INTO `` (`rowid`, `id`) SELECT `rowid`, `id` FROM `t`', + "SELECT 1 FROM main.sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'", 'DROP TABLE `t`', 'ALTER TABLE `` RENAME TO `t`', 'PRAGMA foreign_key_check', @@ -1134,6 +1141,7 @@ public function testAlterTableAddAndDropColumns(): void { 'PRAGMA foreign_keys = OFF', 'CREATE TABLE `` ( `b` INTEGER )' . $this->strict_suffix, 'INSERT INTO `` (`rowid`) SELECT `rowid` FROM `t`', + "SELECT 1 FROM main.sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'", 'DROP TABLE `t`', 'ALTER TABLE `` RENAME TO `t`', 'PRAGMA foreign_key_check', @@ -1170,6 +1178,7 @@ public function testAlterTableDropAndAddSingleColumn(): void { 'PRAGMA foreign_keys = OFF', 'CREATE TABLE `` ( `a` INTEGER )' . $this->strict_suffix, 'INSERT INTO `` (`rowid`) SELECT `rowid` FROM `t`', + "SELECT 1 FROM main.sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'", 'DROP TABLE `t`', 'ALTER TABLE `` RENAME TO `t`', 'PRAGMA foreign_key_check',