From be0bdf8e6f7185fb171f4fcef23fee1284e1c1db Mon Sep 17 00:00:00 2001 From: Samuel de Rougemont Date: Mon, 12 Oct 2026 17:46:33 +0200 Subject: [PATCH 1/3] Fix temporary table lookup without stringified fetches and with a different case The temporary table lookup compared the fetched value to the string "1". PDO SQLite returns native types unless fetches are stringified, so on PHP 8.1 and newer, the lookup never found a temporary table for callers that don't set PDO::ATTR_STRINGIFY_FETCHES. Statements like ALTER TABLE, SHOW COLUMNS, or DESCRIBE then failed or used a standard table of the same name instead. The lookup also matched the table name case-sensitively, unlike the information schema tables, so a temporary table was not found when the statement used a differently cased name. Match the name case-insensitively and check the fetched value for presence rather than comparing it to a string. Co-Authored-By: Claude Fable 5.1 --- ...s-wp-sqlite-information-schema-builder.php | 8 +++-- .../tests/WP_MySQL_On_SQLite_Tests.php | 31 +++++++++++++++++++ .../WP_MySQL_On_SQLite_Translation_Tests.php | 2 +- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php index 12b34be10..0b4540d37 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php @@ -381,12 +381,16 @@ public function temporary_table_exists( string $table_name ): bool { /* * We could search in the "{$this->temporary_table_prefix}tables" table, * but it may not exist yet, so using "sqlite_temp_master" is simpler. + * + * The name is matched case-insensitively, as it is in the information + * schema tables. The fetched value is not compared to a string, because + * PDO SQLite returns native types when fetches are not stringified. */ $stmt = $this->connection->query( - "SELECT 1 FROM sqlite_temp_master WHERE type = 'table' AND name = ?", + "SELECT 1 FROM sqlite_temp_master WHERE type = 'table' AND name = ? COLLATE NOCASE", array( $table_name ) ); - return $stmt->fetchColumn() === '1'; + return false !== $stmt->fetchColumn(); } /** 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..15957870d 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 @@ -5842,6 +5842,37 @@ public function getInformationSchemaIsReadonlyWithUseTestData(): array { ); } + public function testTemporaryTableIsFoundWithoutStringifiedFetches(): void { + // PDO SQLite returns native types unless fetches are stringified. + $this->engine->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, false ); + + // Create a standard and a temporary table with the same name. + $this->assertQuery( 'CREATE TABLE t (a INT)' ); + $this->assertQuery( 'CREATE TEMPORARY TABLE t (b INT)' ); + + // ALTER TABLE will use the temporary table. + $this->assertQuery( 'ALTER TABLE t ADD COLUMN c INT' ); + $result = $this->assertQuery( 'SHOW COLUMNS FROM t' ); + $this->assertEquals( 'b', $result[0]->Field ); + $this->assertEquals( 'c', $result[1]->Field ); + + // The standard table is unchanged. + $this->assertQuery( 'DROP TEMPORARY TABLE t' ); + $result = $this->assertQuery( 'SHOW COLUMNS FROM t' ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'a', $result[0]->Field ); + } + + public function testTemporaryTableIsFoundByDifferentlyCasedName(): void { + $this->assertQuery( 'CREATE TEMPORARY TABLE t (a INT)' ); + + // ALTER TABLE will use the temporary table. + $this->assertQuery( 'ALTER TABLE T ADD COLUMN b INT' ); + $result = $this->assertQuery( 'SHOW COLUMNS FROM T' ); + $this->assertEquals( 'a', $result[0]->Field ); + $this->assertEquals( 'b', $result[1]->Field ); + } + public function testTemporaryTableHasPriorityOverStandardTable(): void { // Create a standard and a temporary table with the same name. $this->assertQuery( 'CREATE TABLE t (a INT, INDEX ia(a))' ); 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 020f3adc9..ec24ac419 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 @@ -2157,7 +2157,7 @@ private function assertQuery( $expected, string $query ): void { array_filter( $executed_queries, function ( $query ) { - return "SELECT 1 FROM sqlite_temp_master WHERE type = 'table' AND name = ?" !== $query; + return "SELECT 1 FROM sqlite_temp_master WHERE type = 'table' AND name = ? COLLATE NOCASE" !== $query; } ) ); From 070b2d27b7b1bf64c27844cd2a0e0d2b22c5d99a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jakes=CC=8C?= Date: Tue, 13 Oct 2026 15:01:25 +0200 Subject: [PATCH 2/3] Ignore temporary tables named like information schema tables When translating a reference to an information schema table, the driver checked whether a temporary table with the same name existed, such as "columns". If it did, the query read the temporary table metadata instead of the standard one, so all standard tables disappeared from the result. With the case-insensitive temporary table lookup, any differently cased name triggers this too, e.g., a temporary table named "Columns". Temporary tables are not listed in the information schema, so always read the standard table metadata. --- .../src/sqlite/class-wp-mysql-on-sqlite.php | 4 +-- .../tests/WP_MySQL_On_SQLite_Tests.php | 25 +++++++++++++++++++ 2 files changed, 27 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 37d4a4c78..391a1233e 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 @@ -5466,8 +5466,8 @@ private function translate_table_ref( WP_Parser_Node $node ): string { } } - $table_is_temporary = $this->information_schema_builder->temporary_table_exists( $table_name ); - $sqlite_table_name = $this->information_schema_builder->get_table_name( $table_is_temporary, $table_name ); + // Temporary tables are not listed in the information schema. + $sqlite_table_name = $this->information_schema_builder->get_table_name( false, $table_name ); // We need to fetch the SQLite column information, because the information // schema tables don't contain records for the information schema itself. 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 15957870d..629e489b8 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 @@ -5923,6 +5923,31 @@ public function testTemporaryTableHasPriorityOverStandardTable(): void { $result = $this->assertQuery( 'SHOW COLUMNS FROM t' ); } + public function testTemporaryTableNamedLikeInformationSchemaTable(): void { + // Create a standard table and temporary tables named like information schema tables. + $this->assertQuery( 'CREATE TABLE t (a INT, INDEX ia(a))' ); + $this->assertQuery( 'CREATE TEMPORARY TABLE tables (b INT)' ); + $this->assertQuery( 'CREATE TEMPORARY TABLE COLUMNS (b INT)' ); + $this->assertQuery( 'CREATE TEMPORARY TABLE Statistics (b INT, INDEX ib(b))' ); + + // The information schema shows the standard table only. + $table_names = "('t', 'tables', 'columns', 'statistics')"; + + $result = $this->assertQuery( "SELECT table_name FROM information_schema.tables WHERE table_name IN $table_names" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 't', $result[0]->TABLE_NAME ); + + $result = $this->assertQuery( "SELECT table_name, column_name FROM information_schema.columns WHERE table_name IN $table_names" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 't', $result[0]->TABLE_NAME ); + $this->assertEquals( 'a', $result[0]->COLUMN_NAME ); + + $result = $this->assertQuery( "SELECT table_name, index_name FROM information_schema.statistics WHERE table_name IN $table_names" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 't', $result[0]->TABLE_NAME ); + $this->assertEquals( 'ia', $result[0]->INDEX_NAME ); + } + public function testStrictSqlModeNullWithoutDefault(): void { $this->assertQuery( "SET SESSION sql_mode = 'STRICT_ALL_TABLES'" ); From 7a8d62256e915d94797dd95a8e4c437fde3db8e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jakes=CC=8C?= Date: Tue, 13 Oct 2026 15:02:18 +0200 Subject: [PATCH 3/3] Extend temporary table lookup tests Test the differently cased name with a standard table of the same name, including DROP TABLE. Before the lookup fix, DROP TABLE removed the temporary table, but deleted the metadata of the standard table. Run the temporary table priority test with stringified fetches both enabled and disabled instead of keeping a partial copy of it. --- .../tests/WP_MySQL_On_SQLite_Tests.php | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 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 629e489b8..b81d767ec 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 @@ -5842,38 +5842,31 @@ public function getInformationSchemaIsReadonlyWithUseTestData(): array { ); } - public function testTemporaryTableIsFoundWithoutStringifiedFetches(): void { - // PDO SQLite returns native types unless fetches are stringified. - $this->engine->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, false ); - + public function testTemporaryTableIsFoundByDifferentlyCasedName(): void { // Create a standard and a temporary table with the same name. $this->assertQuery( 'CREATE TABLE t (a INT)' ); $this->assertQuery( 'CREATE TEMPORARY TABLE t (b INT)' ); // ALTER TABLE will use the temporary table. - $this->assertQuery( 'ALTER TABLE t ADD COLUMN c INT' ); - $result = $this->assertQuery( 'SHOW COLUMNS FROM t' ); + $this->assertQuery( 'ALTER TABLE T ADD COLUMN c INT' ); + $result = $this->assertQuery( 'SHOW COLUMNS FROM T' ); + $this->assertCount( 2, $result ); $this->assertEquals( 'b', $result[0]->Field ); $this->assertEquals( 'c', $result[1]->Field ); - // The standard table is unchanged. - $this->assertQuery( 'DROP TEMPORARY TABLE t' ); - $result = $this->assertQuery( 'SHOW COLUMNS FROM t' ); + // DROP TABLE will remove the temporary table and keep the standard table. + $this->assertQuery( 'DROP TABLE T' ); + $result = $this->assertQuery( 'SHOW COLUMNS FROM T' ); $this->assertCount( 1, $result ); $this->assertEquals( 'a', $result[0]->Field ); } - public function testTemporaryTableIsFoundByDifferentlyCasedName(): void { - $this->assertQuery( 'CREATE TEMPORARY TABLE t (a INT)' ); - - // ALTER TABLE will use the temporary table. - $this->assertQuery( 'ALTER TABLE T ADD COLUMN b INT' ); - $result = $this->assertQuery( 'SHOW COLUMNS FROM T' ); - $this->assertEquals( 'a', $result[0]->Field ); - $this->assertEquals( 'b', $result[1]->Field ); - } + /** + * @dataProvider stringifyFetchesProvider + */ + public function testTemporaryTableHasPriorityOverStandardTable( bool $stringify_fetches ): void { + $this->engine->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, $stringify_fetches ); - public function testTemporaryTableHasPriorityOverStandardTable(): void { // Create a standard and a temporary table with the same name. $this->assertQuery( 'CREATE TABLE t (a INT, INDEX ia(a))' ); $this->assertQuery( 'CREATE TEMPORARY TABLE t (b INT, INDEX ib(b))' ); @@ -5914,6 +5907,7 @@ public function testTemporaryTableHasPriorityOverStandardTable(): void { // First DROP TABLE removes the temporary table. $this->assertQuery( 'DROP TABLE t' ); $result = $this->assertQuery( 'SHOW COLUMNS FROM t' ); + $this->assertCount( 1, $result ); $this->assertEquals( 'a', $result[0]->Field ); // Second DROP TABLE removes the standard table. @@ -5923,6 +5917,13 @@ public function testTemporaryTableHasPriorityOverStandardTable(): void { $result = $this->assertQuery( 'SHOW COLUMNS FROM t' ); } + public function stringifyFetchesProvider(): array { + return array( + 'stringification disabled' => array( false ), + 'stringification enabled' => array( true ), + ); + } + public function testTemporaryTableNamedLikeInformationSchemaTable(): void { // Create a standard table and temporary tables named like information schema tables. $this->assertQuery( 'CREATE TABLE t (a INT, INDEX ia(a))' );