From dfb5cab24c5071995f47273161d65cf4ba56ad25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jakes=CC=8C?= Date: Fri, 23 Oct 2026 13:51:19 +0200 Subject: [PATCH 1/2] Preserve auto-increment counters when rebuilding tables Transfer the original sequence to the replacement table before dropping the old table, keeping the higher counter from copied rows. Keep temporary and persistent sequences separate. Cover table rebuilds, empty and seeded tables, large IDs, explicit counter changes, rollback, and temporal-default upgrade migrations. Fixes https://github.com/WordPress/sqlite-database-integration/issues/544 --- .../src/sqlite/class-wp-mysql-on-sqlite.php | 18 +++ .../WP_MySQL_On_SQLite_Metadata_Tests.php | 143 ++++++++++++++++++ .../tests/WP_MySQL_On_SQLite_Tests.php | 18 +++ .../WP_MySQL_On_SQLite_Translation_Tests.php | 9 ++ 4 files changed, 188 insertions(+) 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..22c1d0a39 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,24 @@ 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), 0)) + WHERE name = ?', + $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', From 270d7404c0819fe7dfdf2729bb0707f31512216a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 23 Oct 2026 21:13:48 +0200 Subject: [PATCH 2/2] Explain the original and replacement sequence rows --- .../src/sqlite/class-wp-mysql-on-sqlite.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 22c1d0a39..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 @@ -5788,8 +5788,11 @@ private function recreate_table_from_information_schema( $this->execute_sqlite_query( sprintf( 'UPDATE %s.sqlite_sequence - SET seq = MAX(seq, COALESCE((SELECT seq FROM %s.sqlite_sequence WHERE name = ? COLLATE NOCASE), 0)) - WHERE name = ?', + 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 ),