From 6229edb8bd4ce8d2a80835e1071cd65aff3ff15e Mon Sep 17 00:00:00 2001 From: Samuel de Rougemont Date: Mon, 12 Oct 2026 18:54:47 +0200 Subject: [PATCH 1/2] Fix unique keys and foreign key restore without stringified fetches ALTER TABLE and SHOW CREATE TABLE compared fetched metadata to string values: NON_UNIQUE to "0" and PRAGMA foreign_keys to "1". PDO SQLite returns native types unless fetches are stringified, so on PHP 8.1 and newer, callers that don't set PDO::ATTR_STRINGIFY_FETCHES got every UNIQUE KEY created as a plain index, and duplicate rows were accepted. ALTER TABLE also skipped its foreign key check. Compare the values as integers. Fixes https://github.com/WordPress/sqlite-database-integration/issues/451 together with https://github.com/WordPress/sqlite-database-integration/pull/522. Co-Authored-By: Claude Opus 5.5 --- .../src/sqlite/class-wp-mysql-on-sqlite.php | 6 +++--- .../tests/WP_MySQL_On_SQLite_Tests.php | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 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 37d4a4c78..4136cd329 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 @@ -5638,7 +5638,7 @@ private function recreate_table_from_information_schema( } // 7. If foreign key constraints were enabled, verify and enable them. - if ( '1' === $pragma_foreign_keys ) { + if ( 1 === (int) $pragma_foreign_keys ) { $this->execute_sqlite_query( 'PRAGMA foreign_key_check' ); $this->execute_sqlite_query( 'PRAGMA foreign_keys = ON' ); } @@ -7130,7 +7130,7 @@ function ( $column ) { } $rows[] = sprintf( ' PRIMARY KEY (%s)', implode( ', ', $column_list ) ); } else { - $is_unique = '0' === $info['NON_UNIQUE']; + $is_unique = 0 === (int) $info['NON_UNIQUE']; // Prefix the original index name with the table name. // This is to avoid conflicting index names in SQLite. @@ -7416,7 +7416,7 @@ private function get_mysql_create_table_statement( bool $table_is_temporary, str $sql .= implode( ', ', array_map( $format_index_column, $constraint ) ); $sql .= ')'; } else { - $is_unique = '0' === $info['NON_UNIQUE']; + $is_unique = 0 === (int) $info['NON_UNIQUE']; $sql = sprintf( ' %s%s%sKEY ', 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 a5c927824..e408a3865 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 @@ -5062,6 +5062,23 @@ public function dataLikeBinaryPatterns(): array { ); } + public function testUniqueKeysWithoutStringifiedFetches(): void { + // PDO SQLite returns native types unless fetches are stringified. + $this->engine->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, false ); + + $this->assertQuery( 'CREATE TABLE t (id INT, email VARCHAR(50), UNIQUE KEY ue (email))' ); + $result = $this->assertQuery( 'SHOW CREATE TABLE t' ); + $this->assertStringContainsString( 'UNIQUE KEY `ue` (`email`)', $result[0]->{'Create Table'} ); + + // The unique key is kept when ALTER TABLE recreates the table. + $this->assertQuery( 'ALTER TABLE t ADD COLUMN name TEXT' ); + $this->assertQuery( "INSERT INTO t (id, email) VALUES (1, 'a@example.com')" ); + + $this->expectException( 'WP_MySQL_On_SQLite_Exception' ); + $this->expectExceptionMessage( 'UNIQUE constraint failed: t.email' ); + $this->assertQuery( "INSERT INTO t (id, email) VALUES (2, 'a@example.com')" ); + } + public function testUniqueConstraints() { $this->assertQuery( "CREATE TABLE _tmp_table ( From 6f7c5d45480f72cac9cda9a98f5dd79cbcdc14fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jakes=CC=8C?= Date: Wed, 14 Oct 2026 15:42:50 +0200 Subject: [PATCH 2/2] Test unique keys and foreign key restore in both stringify modes Run the unique key test with stringified fetches enabled and disabled. Also cover the PRAGMA foreign_keys restore. Its effect is not observable inside the wrapper transaction, but the translation tests record the SQLite queries, so they can check that the foreign key check and restore run in both modes. --- .../tests/WP_MySQL_On_SQLite_Tests.php | 8 +++++--- .../tests/WP_MySQL_On_SQLite_Translation_Tests.php | 13 ++++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) 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 13a0f24ee..3f1a57e6c 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 @@ -5124,9 +5124,11 @@ public function dataLikeBinaryPatterns(): array { ); } - public function testUniqueKeysWithoutStringifiedFetches(): void { - // PDO SQLite returns native types unless fetches are stringified. - $this->engine->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, false ); + /** + * @dataProvider stringifyFetchesProvider + */ + public function testStringifyFetchesDoesNotAffectUniqueKeys( bool $stringify_fetches ): void { + $this->engine->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, $stringify_fetches ); $this->assertQuery( 'CREATE TABLE t (id INT, email VARCHAR(50), UNIQUE KEY ue (email))' ); $result = $this->assertQuery( 'SHOW CREATE TABLE t' ); 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 ec24ac419..d6720e5b6 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 @@ -884,7 +884,11 @@ public function testDropTemporaryTable(): void { ); } - public function testAlterTableAddColumn(): void { + /** + * @dataProvider stringifyFetchesProvider + */ + public function testAlterTableAddColumn( bool $stringify_fetches ): void { + $this->driver->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, $stringify_fetches ); $this->driver->query( 'CREATE TABLE t (id INT)' ); $this->assertQuery( array( @@ -915,6 +919,13 @@ public function testAlterTableAddColumn(): void { ); } + public function stringifyFetchesProvider(): array { + return array( + 'stringification disabled' => array( false ), + 'stringification enabled' => array( true ), + ); + } + public function testAlterTableAddColumnWithNotNull(): void { $this->driver->query( 'CREATE TABLE t (id INT)' ); $this->assertQuery(