diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php index 7dc380dd..befccbcc 100644 --- a/tests/WP_SQLite_Driver_Tests.php +++ b/tests/WP_SQLite_Driver_Tests.php @@ -2834,7 +2834,7 @@ public function testShowGrantsFor() { $result, array( (object) array( - 'Grants for root@localhost' => 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON *.* TO `root`@`localhost` WITH GRANT OPTION', + 'Grants for root@%' => 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON *.* TO `root`@`localhost` WITH GRANT OPTION', ), ) ); @@ -4204,7 +4204,7 @@ public function getReservedPrefixTestData(): array { public function testInformationSchemaIsReadonly( string $query ): void { $this->assertQuery( 'CREATE TABLE tables (id INT)' ); $this->expectException( WP_SQLite_Driver_Exception::class ); - $this->expectExceptionMessage( "Access denied for user 'sqlite'@'%' to database 'information_schema'" ); + $this->expectExceptionMessage( "Access denied for user 'root'@'%' to database 'information_schema'" ); $this->assertQuery( $query ); } @@ -4236,7 +4236,7 @@ public function getInformationSchemaIsReadonlyTestData(): array { public function testInformationSchemaIsReadonlyWithUse( string $query ): void { $this->assertQuery( 'CREATE TABLE tables (id INT)' ); $this->expectException( WP_SQLite_Driver_Exception::class ); - $this->expectExceptionMessage( "Access denied for user 'sqlite'@'%' to database 'information_schema'" ); + $this->expectExceptionMessage( "Access denied for user 'root'@'%' to database 'information_schema'" ); $this->assertQuery( 'USE information_schema' ); $this->assertQuery( $query ); } @@ -9809,4 +9809,248 @@ public function testWriteWithUsageOfInformationSchemaTables(): void { $result ); } + + public function testNonEmptyColumnMeta(): void { + $this->assertQuery( 'CREATE TABLE t (id INT PRIMARY KEY)' ); + $this->assertQuery( 'INSERT INTO t VALUES (1)' ); + + // SELECT + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertSame( 1, $this->engine->get_last_column_count() ); + $this->assertSame( 'id', $this->engine->get_last_column_meta()[0]['name'] ); + + // SHOW COLLATION + $this->assertQuery( 'SHOW COLLATION' ); + $this->assertSame( 7, $this->engine->get_last_column_count() ); + $this->assertSame( 'Collation', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 'Charset', $this->engine->get_last_column_meta()[1]['name'] ); + $this->assertSame( 'Id', $this->engine->get_last_column_meta()[2]['name'] ); + $this->assertSame( 'Default', $this->engine->get_last_column_meta()[3]['name'] ); + $this->assertSame( 'Compiled', $this->engine->get_last_column_meta()[4]['name'] ); + $this->assertSame( 'Sortlen', $this->engine->get_last_column_meta()[5]['name'] ); + $this->assertSame( 'Pad_attribute', $this->engine->get_last_column_meta()[6]['name'] ); + + // SHOW DATABASES + $this->assertQuery( 'SHOW DATABASES' ); + $this->assertSame( 1, $this->engine->get_last_column_count() ); + $this->assertSame( 'Database', $this->engine->get_last_column_meta()[0]['name'] ); + + // SHOW CREATE TABLE + $this->assertQuery( 'SHOW CREATE TABLE t' ); + $this->assertSame( 2, $this->engine->get_last_column_count() ); + $this->assertSame( 'Table', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 'Create Table', $this->engine->get_last_column_meta()[1]['name'] ); + + // SHOW TABLE STATUS + $this->assertQuery( 'SHOW TABLE STATUS' ); + $this->assertSame( 18, $this->engine->get_last_column_count() ); + $this->assertSame( 'Name', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 'Engine', $this->engine->get_last_column_meta()[1]['name'] ); + $this->assertSame( 'Version', $this->engine->get_last_column_meta()[2]['name'] ); + $this->assertSame( 'Row_format', $this->engine->get_last_column_meta()[3]['name'] ); + $this->assertSame( 'Rows', $this->engine->get_last_column_meta()[4]['name'] ); + $this->assertSame( 'Avg_row_length', $this->engine->get_last_column_meta()[5]['name'] ); + $this->assertSame( 'Data_length', $this->engine->get_last_column_meta()[6]['name'] ); + $this->assertSame( 'Max_data_length', $this->engine->get_last_column_meta()[7]['name'] ); + $this->assertSame( 'Index_length', $this->engine->get_last_column_meta()[8]['name'] ); + $this->assertSame( 'Data_free', $this->engine->get_last_column_meta()[9]['name'] ); + $this->assertSame( 'Auto_increment', $this->engine->get_last_column_meta()[10]['name'] ); + $this->assertSame( 'Create_time', $this->engine->get_last_column_meta()[11]['name'] ); + $this->assertSame( 'Update_time', $this->engine->get_last_column_meta()[12]['name'] ); + $this->assertSame( 'Check_time', $this->engine->get_last_column_meta()[13]['name'] ); + $this->assertSame( 'Collation', $this->engine->get_last_column_meta()[14]['name'] ); + $this->assertSame( 'Checksum', $this->engine->get_last_column_meta()[15]['name'] ); + $this->assertSame( 'Create_options', $this->engine->get_last_column_meta()[16]['name'] ); + $this->assertSame( 'Comment', $this->engine->get_last_column_meta()[17]['name'] ); + + // SHOW TABLES + $this->assertQuery( 'SHOW TABLES' ); + $this->assertSame( 1, $this->engine->get_last_column_count() ); + $this->assertSame( 'Tables_in_wp', $this->engine->get_last_column_meta()[0]['name'] ); + + // SHOW FULL TABLES + $this->assertQuery( 'SHOW FULL TABLES' ); + $this->assertSame( 2, $this->engine->get_last_column_count() ); + $this->assertSame( 'Tables_in_wp', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 'Table_type', $this->engine->get_last_column_meta()[1]['name'] ); + + // SHOW COLUMNS + $this->assertQuery( 'SHOW COLUMNS FROM t' ); + $this->assertSame( 6, $this->engine->get_last_column_count() ); + $this->assertSame( 'Field', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 'Type', $this->engine->get_last_column_meta()[1]['name'] ); + $this->assertSame( 'Null', $this->engine->get_last_column_meta()[2]['name'] ); + $this->assertSame( 'Key', $this->engine->get_last_column_meta()[3]['name'] ); + $this->assertSame( 'Default', $this->engine->get_last_column_meta()[4]['name'] ); + $this->assertSame( 'Extra', $this->engine->get_last_column_meta()[5]['name'] ); + + // SHOW INDEX + $this->assertQuery( 'SHOW INDEX FROM t' ); + $this->assertSame( 15, $this->engine->get_last_column_count() ); + $this->assertSame( 'Table', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 'Non_unique', $this->engine->get_last_column_meta()[1]['name'] ); + $this->assertSame( 'Key_name', $this->engine->get_last_column_meta()[2]['name'] ); + $this->assertSame( 'Seq_in_index', $this->engine->get_last_column_meta()[3]['name'] ); + $this->assertSame( 'Column_name', $this->engine->get_last_column_meta()[4]['name'] ); + $this->assertSame( 'Collation', $this->engine->get_last_column_meta()[5]['name'] ); + $this->assertSame( 'Cardinality', $this->engine->get_last_column_meta()[6]['name'] ); + $this->assertSame( 'Sub_part', $this->engine->get_last_column_meta()[7]['name'] ); + $this->assertSame( 'Packed', $this->engine->get_last_column_meta()[8]['name'] ); + $this->assertSame( 'Null', $this->engine->get_last_column_meta()[9]['name'] ); + $this->assertSame( 'Index_type', $this->engine->get_last_column_meta()[10]['name'] ); + $this->assertSame( 'Comment', $this->engine->get_last_column_meta()[11]['name'] ); + $this->assertSame( 'Index_comment', $this->engine->get_last_column_meta()[12]['name'] ); + $this->assertSame( 'Visible', $this->engine->get_last_column_meta()[13]['name'] ); + $this->assertSame( 'Expression', $this->engine->get_last_column_meta()[14]['name'] ); + + // SHOW GRANTS + $this->assertQuery( 'SHOW GRANTS' ); + $this->assertSame( 1, $this->engine->get_last_column_count() ); + $this->assertSame( 'Grants for root@%', $this->engine->get_last_column_meta()[0]['name'] ); + + // SHOW VARIABLES + $this->assertQuery( 'SHOW VARIABLES' ); + $this->assertSame( 2, $this->engine->get_last_column_count() ); + $this->assertSame( 'Variable_name', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 'Value', $this->engine->get_last_column_meta()[1]['name'] ); + + // DESCRIBE/EXPLAIN + $this->assertQuery( 'DESCRIBE t' ); + $this->assertSame( 6, $this->engine->get_last_column_count() ); + $this->assertSame( 'Field', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 'Type', $this->engine->get_last_column_meta()[1]['name'] ); + $this->assertSame( 'Null', $this->engine->get_last_column_meta()[2]['name'] ); + $this->assertSame( 'Key', $this->engine->get_last_column_meta()[3]['name'] ); + $this->assertSame( 'Default', $this->engine->get_last_column_meta()[4]['name'] ); + $this->assertSame( 'Extra', $this->engine->get_last_column_meta()[5]['name'] ); + + // ANALYZE TABLE + $this->assertQuery( 'ANALYZE TABLE t' ); + $this->assertSame( 4, $this->engine->get_last_column_count() ); + $this->assertSame( 'Table', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 'Op', $this->engine->get_last_column_meta()[1]['name'] ); + $this->assertSame( 'Msg_type', $this->engine->get_last_column_meta()[2]['name'] ); + $this->assertSame( 'Msg_text', $this->engine->get_last_column_meta()[3]['name'] ); + + // CHECK TABLE + $this->assertQuery( 'CHECK TABLE t' ); + $this->assertSame( 4, $this->engine->get_last_column_count() ); + $this->assertSame( 'Table', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 'Op', $this->engine->get_last_column_meta()[1]['name'] ); + $this->assertSame( 'Msg_type', $this->engine->get_last_column_meta()[2]['name'] ); + $this->assertSame( 'Msg_text', $this->engine->get_last_column_meta()[3]['name'] ); + + // OPTIMIZE TABLE + $this->assertQuery( 'OPTIMIZE TABLE t' ); + $this->assertSame( 4, $this->engine->get_last_column_count() ); + $this->assertSame( 'Table', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 'Op', $this->engine->get_last_column_meta()[1]['name'] ); + $this->assertSame( 'Msg_type', $this->engine->get_last_column_meta()[2]['name'] ); + $this->assertSame( 'Msg_text', $this->engine->get_last_column_meta()[3]['name'] ); + + // REPAIR TABLE + $this->assertQuery( 'REPAIR TABLE t' ); + $this->assertSame( 4, $this->engine->get_last_column_count() ); + $this->assertSame( 'Table', $this->engine->get_last_column_meta()[0]['name'] ); + $this->assertSame( 'Op', $this->engine->get_last_column_meta()[1]['name'] ); + $this->assertSame( 'Msg_type', $this->engine->get_last_column_meta()[2]['name'] ); + $this->assertSame( 'Msg_text', $this->engine->get_last_column_meta()[3]['name'] ); + } + + public function testEmptyColumnMeta(): void { + // CREATE TABLE + $this->assertQuery( 'CREATE TABLE t (id INT)' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // INSERT + $this->assertQuery( 'INSERT INTO t (id) VALUES (1)' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // REPLACE + $this->assertQuery( 'UPDATE t SET id = 1' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // DELETE + $this->assertQuery( 'DELETE FROM t' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // TRUNCATE TABLE + $this->assertQuery( 'TRUNCATE TABLE t' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // START TRANSACTION + $this->assertQuery( 'START TRANSACTION' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // COMMIT + $this->assertQuery( 'COMMIT' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // ROLLBACK + $this->assertQuery( 'ROLLBACK' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // SAVEPOINT + $this->assertQuery( 'SAVEPOINT s1' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // ROLLBACK TO SAVEPOINT + $this->assertQuery( 'ROLLBACK TO SAVEPOINT s1' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // RELEASE SAVEPOINT + $this->assertQuery( 'RELEASE SAVEPOINT s1' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // LOCK TABLE + $this->assertQuery( 'LOCK TABLES t READ' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // UNLOCK TABLE + $this->assertQuery( 'UNLOCK TABLES' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // ALTER TABLE + $this->assertQuery( 'ALTER TABLE t ADD COLUMN name VARCHAR(255)' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // CREATE INDEX + $this->assertQuery( 'CREATE INDEX idx_name ON t (name)' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // DROP INDEX + $this->assertQuery( 'DROP INDEX idx_name ON t' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // DROP TABLE + $this->assertQuery( 'DROP TABLE t' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // USE + $this->assertQuery( 'USE wp' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + + // SET + $this->assertQuery( 'SET @my_var = 1' ); + $this->assertSame( 0, $this->engine->get_last_column_count() ); + $this->assertSame( array(), $this->engine->get_last_column_meta() ); + } } diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index 00630d76..134400cc 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -1487,32 +1487,7 @@ private function execute_select_statement( WP_Parser_Node $node ): void { // Store column meta info. This must be done before fetching data, which // seems to erase type information for expressions in the SELECT clause. - $this->last_column_meta = array(); - for ( $i = 0; $i < $stmt->columnCount(); $i++ ) { - /* - * Workaround for PHP PDO SQLite bug (#79664) in PHP < 7.3. - * See also: https://github.com/php/php-src/pull/5654 - */ - if ( PHP_VERSION_ID < 70300 ) { - try { - $this->last_column_meta[] = $stmt->getColumnMeta( $i ); - } catch ( Throwable $e ) { - $this->last_column_meta[] = array( - 'native_type' => 'null', - 'pdo_type' => PDO::PARAM_NULL, - 'flags' => array(), - 'table' => '', - 'name' => '', - 'len' => -1, - 'precision' => 0, - ); - } - continue; - } - - $this->last_column_meta[] = $stmt->getColumnMeta( $i ); - } - + $this->store_last_column_meta_from_statement( $stmt ); $this->set_results_from_fetched_data( $stmt->fetchAll( $this->pdo_fetch_mode ) ); @@ -2326,6 +2301,27 @@ private function execute_show_statement( WP_Parser_Node $node ): void { ) ); } + + $this->last_column_meta = array( + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'Table', + 'len' => 256, + 'precision' => 31, + ), + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'Create Table', + 'len' => strlen( $sql ), + 'precision' => 31, + ), + ); return; } break; @@ -2338,10 +2334,21 @@ private function execute_show_statement( WP_Parser_Node $node ): void { $this->set_results_from_fetched_data( array( (object) array( - 'Grants for root@localhost' => 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON *.* TO `root`@`localhost` WITH GRANT OPTION', + 'Grants for root@%' => 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON *.* TO `root`@`localhost` WITH GRANT OPTION', ), ) ); + $this->last_column_meta = array( + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'Grants for root@%', + 'len' => 4096, + 'precision' => 31, + ), + ); return; case WP_MySQL_Lexer::TABLE_SYMBOL: $this->execute_show_table_status_statement( $node ); @@ -2350,7 +2357,27 @@ private function execute_show_statement( WP_Parser_Node $node ): void { $this->execute_show_tables_statement( $node ); return; case WP_MySQL_Lexer::VARIABLES_SYMBOL: - $this->last_result = true; + $this->last_result = true; + $this->last_column_meta = array( + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'not_null' ), + 'table' => 'session_variables', + 'name' => 'Variable_name', + 'len' => 256, + 'precision' => 0, + ), + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 'session_variables', + 'name' => 'Value', + 'len' => 4096, + 'precision' => 0, + ), + ); return; } @@ -2372,21 +2399,22 @@ private function execute_show_collation_statement(): void { // TODO: LIKE and WHERE clauses. - $result = $this->execute_sqlite_query( $definition )->fetchAll( PDO::FETCH_ASSOC ); - - $collations = array(); - foreach ( $result as $row ) { - $collations[] = (object) array( - 'Collation' => $row['COLLATION_NAME'], - 'Charset' => $row['CHARACTER_SET_NAME'], - 'Id' => $row['ID'], - 'Default' => $row['IS_DEFAULT'], - 'Compiled' => $row['IS_COMPILED'], - 'Sortlen' => $row['SORTLEN'], - 'Pad_attribute' => $row['PAD_ATTRIBUTE'], - ); - } - $this->set_results_from_fetched_data( $collations ); + $stmt = $this->execute_sqlite_query( + sprintf( + 'SELECT + COLLATION_NAME AS `Collation`, + CHARACTER_SET_NAME AS `Charset`, + ID AS `Id`, + IS_DEFAULT AS `Default`, + IS_COMPILED AS `Compiled`, + SORTLEN AS `Sortlen`, + PAD_ATTRIBUTE AS `Pad_attribute` + FROM (%s)', + $definition + ) + ); + $this->store_last_column_meta_from_statement( $stmt ); + $this->set_results_from_fetched_data( $stmt->fetchAll( PDO::FETCH_OBJ ) ); } /** @@ -2403,7 +2431,7 @@ private function execute_show_databases_statement( WP_Parser_Node $node ): void $condition = $this->translate_show_like_or_where_condition( $like_or_where, 'schema_name' ); } - $databases = $this->execute_sqlite_query( + $stmt = $this->execute_sqlite_query( sprintf( 'SELECT SCHEMA_NAME AS Database FROM ( @@ -2416,8 +2444,10 @@ private function execute_show_databases_statement( WP_Parser_Node $node ): void $this->get_saved_db_name(), $this->main_db_name, ) - )->fetchAll( PDO::FETCH_OBJ ); + ); + $this->store_last_column_meta_from_statement( $stmt ); + $databases = $stmt->fetchAll( PDO::FETCH_OBJ ); $this->set_results_from_fetched_data( $databases ); } @@ -2462,7 +2492,7 @@ private function execute_show_index_statement( WP_Parser_Node $node ): void { */ $statistics_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'statistics' ); - $index_info = $this->execute_sqlite_query( + $stmt = $this->execute_sqlite_query( ' SELECT TABLE_NAME AS `Table`, @@ -2493,8 +2523,10 @@ private function execute_show_index_statement( WP_Parser_Node $node ): void { SEQ_IN_INDEX ", array( $this->get_saved_db_name( $database ), $table_name ) - )->fetchAll( PDO::FETCH_OBJ ); + ); + $this->store_last_column_meta_from_statement( $stmt ); + $index_info = $stmt->fetchAll( PDO::FETCH_OBJ ); $this->set_results_from_fetched_data( $index_info ); } @@ -2526,45 +2558,42 @@ private function execute_show_table_status_statement( WP_Parser_Node $node ): vo false, // SHOW TABLE STATUS lists only non-temporary tables. 'tables' ); - $table_info = $this->execute_sqlite_query( + $stmt = $this->execute_sqlite_query( sprintf( - 'SELECT * FROM %s WHERE table_schema = ? %s ORDER BY table_name', + 'SELECT + table_name AS `Name`, + engine AS `Engine`, + version AS `Version`, + row_format AS `Row_format`, + table_rows AS `Rows`, + avg_row_length AS `Avg_row_length`, + data_length AS `Data_length`, + max_data_length AS `Max_data_length`, + index_length AS `Index_length`, + data_free AS `Data_free`, + auto_increment AS `Auto_increment`, + create_time AS `Create_time`, + update_time AS `Update_time`, + check_time AS `Check_time`, + table_collation AS `Collation`, + checksum AS `Checksum`, + create_options AS `Create_options`, + table_comment AS `Comment` + FROM %s + WHERE table_schema = ? %s + ORDER BY table_name', $this->quote_sqlite_identifier( $tables_tables ), $condition ?? '' ), array( $this->get_saved_db_name( $database ) ) - )->fetchAll( PDO::FETCH_ASSOC ); + ); + $this->store_last_column_meta_from_statement( $stmt ); + $table_info = $stmt->fetchAll( PDO::FETCH_OBJ ); if ( false === $table_info ) { $this->set_results_from_fetched_data( array() ); } - - // Format the results. - $tables = array(); - foreach ( $table_info as $value ) { - $tables[] = (object) array( - 'Name' => $value['TABLE_NAME'], - 'Engine' => $value['ENGINE'], - 'Version' => $value['VERSION'], - 'Row_format' => $value['ROW_FORMAT'], - 'Rows' => $value['TABLE_ROWS'], - 'Avg_row_length' => $value['AVG_ROW_LENGTH'], - 'Data_length' => $value['DATA_LENGTH'], - 'Max_data_length' => $value['MAX_DATA_LENGTH'], - 'Index_length' => $value['INDEX_LENGTH'], - 'Data_free' => $value['DATA_FREE'], - 'Auto_increment' => $value['AUTO_INCREMENT'], - 'Create_time' => $value['CREATE_TIME'], - 'Update_time' => $value['UPDATE_TIME'], - 'Check_time' => $value['CHECK_TIME'], - 'Collation' => $value['TABLE_COLLATION'], - 'Checksum' => $value['CHECKSUM'], - 'Create_options' => $value['CREATE_OPTIONS'], - 'Comment' => $value['TABLE_COMMENT'], - ); - } - - $this->set_results_from_fetched_data( $tables ); + $this->set_results_from_fetched_data( $table_info ); } /** @@ -2590,41 +2619,33 @@ private function execute_show_tables_statement( WP_Parser_Node $node ): void { $condition = $this->translate_show_like_or_where_condition( $like_or_where, 'table_name' ); } + // Handle the FULL keyword. + $command_type = $node->get_first_child_node( 'showCommandType' ); + $is_full = $command_type && $command_type->has_child_token( WP_MySQL_Lexer::FULL_SYMBOL ); + // Fetch table information. $table_tables = $this->information_schema_builder->get_table_name( false, // SHOW TABLES lists only non-temporary tables. 'tables' ); - $table_info = $this->execute_sqlite_query( + $stmt = $this->execute_sqlite_query( sprintf( - 'SELECT * FROM %s WHERE table_schema = ? %s ORDER BY table_name', + 'SELECT %s FROM %s WHERE table_schema = ? %s ORDER BY table_name', + $is_full + ? sprintf( 'table_name AS `Tables_in_%s`, table_type AS `Table_type`', $database ) + : sprintf( 'table_name AS `Tables_in_%s`', $database ), $this->quote_sqlite_identifier( $table_tables ), $condition ?? '' ), array( $this->get_saved_db_name( $database ) ) - )->fetchAll( PDO::FETCH_ASSOC ); + ); + $this->store_last_column_meta_from_statement( $stmt ); + $table_info = $stmt->fetchAll( PDO::FETCH_OBJ ); if ( false === $table_info ) { $this->set_results_from_fetched_data( array() ); } - - // Handle the FULL keyword. - $command_type = $node->get_first_child_node( 'showCommandType' ); - $is_full = $command_type && $command_type->has_child_token( WP_MySQL_Lexer::FULL_SYMBOL ); - - // Format the results. - $tables = array(); - foreach ( $table_info as $value ) { - $table = array( - "Tables_in_$database" => $value['TABLE_NAME'], - ); - if ( true === $is_full ) { - $table['Table_type'] = $value['TABLE_TYPE']; - } - $tables[] = (object) $table; - } - - $this->set_results_from_fetched_data( $tables ); + $this->set_results_from_fetched_data( $table_info ); } /** @@ -2674,34 +2695,30 @@ private function execute_show_columns_statement( WP_Parser_Node $node ): void { // Fetch column information. $columns_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'columns' ); - $column_info = $this->execute_sqlite_query( + $stmt = $this->execute_sqlite_query( sprintf( - 'SELECT * FROM %s WHERE table_schema = ? AND table_name = ? %s ORDER BY ordinal_position', + 'SELECT + column_name AS `Field`, + column_type AS `Type`, + is_nullable AS `Null`, + column_key AS `Key`, + column_default AS `Default`, + extra AS `Extra` + FROM %s + WHERE table_schema = ? AND table_name = ? %s + ORDER BY ordinal_position', $this->quote_sqlite_identifier( $columns_table ), $condition ?? '' ), array( $this->get_saved_db_name( $database ), $table_name ) - )->fetchAll( PDO::FETCH_ASSOC ); + ); + $this->store_last_column_meta_from_statement( $stmt ); + $column_info = $stmt->fetchAll( PDO::FETCH_OBJ ); if ( false === $column_info ) { $this->set_results_from_fetched_data( array() ); } - - // Format the results. - $columns = array(); - foreach ( $column_info as $value ) { - $column = array( - 'Field' => $value['COLUMN_NAME'], - 'Type' => $value['COLUMN_TYPE'], - 'Null' => $value['IS_NULLABLE'], - 'Key' => $value['COLUMN_KEY'], - 'Default' => $value['COLUMN_DEFAULT'], - 'Extra' => $value['EXTRA'], - ); - $columns[] = (object) $column; - } - - $this->set_results_from_fetched_data( $columns ); + $this->set_results_from_fetched_data( $column_info ); } /** @@ -2718,7 +2735,7 @@ private function execute_describe_statement( WP_Parser_Node $node ): void { $table_is_temporary = $this->information_schema_builder->temporary_table_exists( $table_name ); $columns_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'columns' ); - $column_info = $this->execute_sqlite_query( + $stmt = $this->execute_sqlite_query( ' SELECT column_name AS `Field`, @@ -2733,8 +2750,10 @@ private function execute_describe_statement( WP_Parser_Node $node ): void { ORDER BY ordinal_position ', array( $this->get_saved_db_name( $database ), $table_name ) - )->fetchAll( PDO::FETCH_OBJ ); + ); + $this->store_last_column_meta_from_statement( $stmt ); + $column_info = $stmt->fetchAll( PDO::FETCH_OBJ ); $this->set_results_from_fetched_data( $column_info ); } @@ -3070,6 +3089,45 @@ private function execute_administration_statement( WP_Parser_Node $node ): void 'Msg_text' => count( $errors ) > 0 ? 'Operation failed' : 'OK', ); } + + $this->last_column_meta = array( + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => '', + 'name' => 'Table', + 'len' => 512, + 'precision' => 31, + ), + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => '', + 'name' => 'Op', + 'len' => 40, + 'precision' => 31, + ), + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => '', + 'name' => 'Msg_type', + 'len' => 40, + 'precision' => 31, + ), + array( + 'native_type' => 'TEXT', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => '', + 'name' => 'Msg_text', + 'len' => 1572864, + 'precision' => 31, + ), + ); $this->set_results_from_fetched_data( $results ); } @@ -4582,6 +4640,42 @@ private function translate_update_list_in_non_strict_mode( string $table_name, W return $fragment; } + /** + * Store column metadata for the last SQLite statement. + * + * This function stores the original SQLite column metadata as-is, without + * converting it into MySQL column metadata. That is done only when needed. + * + * @param PDOStatement $stmt The PDOStatement object containing the SQLite column metadata. + */ + private function store_last_column_meta_from_statement( PDOStatement $stmt ): void { + $this->last_column_meta = array(); + for ( $i = 0; $i < $stmt->columnCount(); $i++ ) { + /* + * Workaround for PHP PDO SQLite bug (#79664) in PHP < 7.3. + * See also: https://github.com/php/php-src/pull/5654 + */ + if ( PHP_VERSION_ID < 70300 ) { + try { + $this->last_column_meta[] = $stmt->getColumnMeta( $i ); + } catch ( Throwable $e ) { + $this->last_column_meta[] = array( + 'native_type' => 'null', + 'pdo_type' => PDO::PARAM_NULL, + 'flags' => array(), + 'table' => '', + 'name' => '', + 'len' => -1, + 'precision' => 0, + ); + } + continue; + } + + $this->last_column_meta[] = $stmt->getColumnMeta( $i ); + } + } + /** * Unnest parenthesized MySQL expression node. * @@ -5796,7 +5890,7 @@ private function new_not_supported_exception( string $cause ): WP_SQLite_Driver_ */ private function new_access_denied_to_information_schema_exception(): WP_SQLite_Driver_Exception { return $this->new_driver_exception( - "Access denied for user 'sqlite'@'%' to database 'information_schema'", + "Access denied for user 'root'@'%' to database 'information_schema'", '42000' ); }