diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php index 0e70528a..930c2641 100644 --- a/tests/WP_SQLite_Driver_Tests.php +++ b/tests/WP_SQLite_Driver_Tests.php @@ -5859,4 +5859,413 @@ public function testDropIndex(): void { $result = $this->engine->execute_sqlite_query( "PRAGMA index_list('t')" )->fetchAll( PDO::FETCH_ASSOC ); $this->assertCount( 0, $result ); } + + public function testColumnInfo(): void { + $this->assertQuery( ' + CREATE TABLE t ( + id INT, + name TEXT, + score DOUBLE, + data BLOB, + PRIMARY KEY (id), + UNIQUE KEY (name) + )' + ); + $this->assertQuery( 'SELECT * FROM t' ); + + $this->assertEquals( 4, $this->engine->get_last_column_count() ); + + $column_info = $this->engine->get_last_column_meta(); + $this->assertCount( 4, $column_info ); + + // INT + $this->assertSame( + array( + 'native_type' => 'LONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'id', + 'len' => 11, + 'precision' => 0, + 'sqlite:decl_type' => 'INT', + ), + $column_info[0] + ); + + $this->assertSame( + array( + 'native_type' => 'BLOB', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'name', + 'len' => 262140, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + ), + $column_info[1] + ); + + $this->assertSame( + array( + 'native_type' => 'DOUBLE', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'score', + 'len' => 22, + 'precision' => 31, + 'sqlite:decl_type' => 'REAL', + ), + $column_info[2] + ); + + $this->assertSame( + array( + 'native_type' => 'BLOB', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'data', + 'len' => 65535, + 'precision' => 0, + 'sqlite:decl_type' => 'BLOB', + ), + $column_info[3] + ); + } + + public function testColumnInfoForIntegerDataTypes(): void { + $this->assertQuery( ' + CREATE TABLE t ( + col_bit BIT, + col_bool BOOL, + col_tinyint TINYINT, + col_smallint SMALLINT, + col_mediumint MEDIUMINT, + col_int INT, + col_bigint BIGINT + )' + ); + + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertEquals( 7, $this->engine->get_last_column_count() ); + + $column_info = $this->engine->get_last_column_meta(); + + // INT + $this->assertSame( + array( + array( + 'native_type' => 'BIT', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_bit', + 'len' => 1, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + ), + array( + 'native_type' => 'TINY', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_bool', + 'len' => 1, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + ), + array( + 'native_type' => 'TINY', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_tinyint', + 'len' => 4, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + ), + array( + 'native_type' => 'SHORT', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_smallint', + 'len' => 6, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + ), + array( + 'native_type' => 'INT24', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_mediumint', + 'len' => 9, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + ), + array( + 'native_type' => 'LONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_int', + 'len' => 11, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + ), + array( + 'native_type' => 'LONGLONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_bigint', + 'len' => 20, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + ), + ), + $column_info + ); + } + + public function testColumnInfoForFloatingPointDataTypes(): void { + $this->assertQuery( ' + CREATE TABLE t ( + col_float FLOAT, + col_double DOUBLE, + col_real REAL, + col_decimal DECIMAL(10,2), + col_dec DEC(10,2), + col_fixed FIXED(10,2), + col_numeric NUMERIC(10,2) + )' + ); + + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertEquals( 7, $this->engine->get_last_column_count() ); + + $column_info = $this->engine->get_last_column_meta(); + + // INT + $this->assertSame( + array( + array( + 'native_type' => 'REAL', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_float', + 'len' => 12, + 'precision' => 31, + 'sqlite:decl_type' => 'REAL', + ), + array( + 'native_type' => 'DOUBLE', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_double', + 'len' => 22, + 'precision' => 31, + 'sqlite:decl_type' => 'REAL', + ), + array( + 'native_type' => 'DOUBLE', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_real', + 'len' => 22, + 'precision' => 31, + 'sqlite:decl_type' => 'REAL', + ), + array( + 'native_type' => 'NEWDECIMAL', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_decimal', + 'len' => 12, + 'precision' => 2, + 'sqlite:decl_type' => 'REAL', + ), + array( + 'native_type' => 'NEWDECIMAL', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_dec', + 'len' => 12, + 'precision' => 2, + 'sqlite:decl_type' => 'REAL', + ), + array( + 'native_type' => 'NEWDECIMAL', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_fixed', + 'len' => 12, + 'precision' => 2, + 'sqlite:decl_type' => 'REAL', + ), + array( + 'native_type' => 'NEWDECIMAL', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_numeric', + 'len' => 12, + 'precision' => 2, + 'sqlite:decl_type' => 'REAL', + ), + ), + $column_info + ); + } + + + public function testColumnInfoForStringDataTypes(): void { + $this->assertQuery( + "CREATE TABLE t ( + col_char CHAR(10), + col_varchar VARCHAR(10), + col_nchar NCHAR(10), + col_nvarchar NVARCHAR(10), + col_tinytext TINYTEXT, + col_text TEXT, + col_mediumtext MEDIUMTEXT, + col_longtext LONGTEXT, + col_enum ENUM('a', 'b', 'c'), + col_set SET('a', 'b', 'c'), + col_json JSON + )" + ); + + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertEquals( 11, $this->engine->get_last_column_count() ); + + $column_info = $this->engine->get_last_column_meta(); + + // INT + $this->assertSame( + array( + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_char', + 'len' => 40, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + ), + array( + 'native_type' => 'VAR_STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_varchar', + 'len' => 40, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + ), + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_nchar', + 'len' => 40, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + ), + array( + 'native_type' => 'VAR_STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_nvarchar', + 'len' => 40, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + ), + array( + 'native_type' => 'BLOB', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_tinytext', + 'len' => 1020, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + ), + array( + 'native_type' => 'BLOB', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_text', + 'len' => 262140, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + ), + array( + 'native_type' => 'BLOB', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_mediumtext', + 'len' => 67108860, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + ), + array( + 'native_type' => 'BLOB', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_longtext', + 'len' => 4294967295, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + ), + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_enum', + 'len' => 4, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + ), + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_set', + 'len' => 20, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + ), + array( + 'native_type' => 'BLOB', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_json', + 'len' => 4294967295, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + ), + ), + $column_info + ); + } } diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index 5d491847..68adb125 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -378,6 +378,13 @@ class WP_SQLite_Driver { */ private $last_return_value; + /** + * Statement object that carries column metadata for the last emulated query. + * + * @var PDOStatement|null + */ + private $last_column_meta_statement; + /** * Number of rows found by the last SQL_CALC_FOUND_ROW query. * @@ -718,6 +725,165 @@ public function get_last_return_value() { return $this->last_return_value; } + /** + * Get the number of columns returned by the last emulated query. + * + * @return int + */ + public function get_last_column_count(): int { + if ( null === $this->last_column_meta_statement ) { + return 0; + } + return $this->last_column_meta_statement->columnCount(); + } + + /** + * Get column metadata for results of the last emulated query. + * + * @return array + */ + public function get_last_column_meta(): array { + if ( null === $this->last_column_meta_statement ) { + return array(); + } + + // Map of SQLite column types to native PHP types. + $sqlite_to_native_types = array( + 'NULL' => 'NULL', + 'INT' => 'LONG', + 'INTEGER' => 'LONG', + 'TEXT' => 'BLOB', + 'REAL' => 'DOUBLE', + 'BLOB' => 'BLOB', + ); + + $mysql_to_native_types = array( + 'bit' => array( 'BIT', 1, 0 ), + 'tinyint' => array( 'TINY', 4, 0 ), + 'smallint' => array( 'SHORT', 6, 0 ), + 'mediumint' => array( 'INT24', 9, 0 ), + 'int' => array( 'LONG', 11, 0 ), + 'bigint' => array( 'LONGLONG', 20, 0 ), + + 'float' => array( 'REAL', 12, 31 ), + 'double' => array( 'DOUBLE', 22, 31 ), + 'decimal' => array( 'NEWDECIMAL', null, null ), + + 'char' => array( 'STRING', null, 0 ), + 'varchar' => array( 'VAR_STRING', null, 0 ), + 'tinytext' => array( 'BLOB', null, 0 ), + 'text' => array( 'BLOB', null, 0 ), + 'mediumtext' => array( 'BLOB', null, 0 ), + 'longtext' => array( 'BLOB', null, 0 ), + 'enum' => array( 'STRING', null, 0 ), + 'set' => array( 'STRING', null, 0 ), + 'json' => array( 'BLOB', 4294967295, 0 ), + ); + + // Map of SQLite column types to PDO parameter types. + $pdo_types = array( + 'NULL' => PDO::PARAM_NULL, + 'INT' => PDO::PARAM_INT, + 'INTEGER' => PDO::PARAM_INT, + 'TEXT' => PDO::PARAM_STR, + 'REAL' => PDO::PARAM_STR, + 'BLOB' => PDO::PARAM_STR, + ); + + // Build the column metadata as per "PDOStatement::getColumnMeta()". + $column_meta = array(); + for ( $i = 0; $i < $this->last_column_meta_statement->columnCount(); $i++ ) { + $meta = $this->last_column_meta_statement->getColumnMeta( $i ); + $table = $meta['table']; + $name = $meta['name']; + + // Types. + $sqlite_type = $meta['sqlite:decl_type']; + $native_type = $sqlite_to_native_types[ $sqlite_type ] ?? null; + $pdo_type = $pdo_types[ $sqlite_type ] ?? PDO::PARAM_NULL; + + // Length and precision. + $len = $meta['len']; + $precision = $meta['precision']; + + // When table is known, we can get data from the information schema. + if ( strlen( $table ) > 0 ) { + $table_is_temporary = $this->information_schema_builder->temporary_table_exists( $table ); + $columns_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'columns' ); + $column_info = $this->execute_sqlite_query( + sprintf( + ' + SELECT + DATA_TYPE, + COLUMN_TYPE, + CHARACTER_MAXIMUM_LENGTH, + NUMERIC_PRECISION, + NUMERIC_SCALE + FROM %s + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ? + ', + $this->quote_sqlite_identifier( $columns_table ) + ), + array( $this->db_name, $table, $name ) + )->fetch( PDO::FETCH_ASSOC ); + + $type_info = $mysql_to_native_types[ $column_info['DATA_TYPE'] ] ?? null; + if ( null !== $type_info ) { + $native_type = $type_info[0]; + $len = $type_info[1]; + $precision = $type_info[2]; + } + + if ( 'tinyint(1)' === $column_info['COLUMN_TYPE'] ) { + $len = 1; + } + + if ( 'decimal' === $column_info['DATA_TYPE'] ) { + $len = (int) $column_info['NUMERIC_PRECISION'] + (int) $column_info['NUMERIC_SCALE']; + $precision = (int) $column_info['NUMERIC_SCALE']; + } + + // If set, lenght can be taken from the information schema. + if ( isset( $column_info['CHARACTER_MAXIMUM_LENGTH'] ) ) { + $len = (int) $column_info['CHARACTER_MAXIMUM_LENGTH']; + } + + // For string types, the length is multiplied by the maximum number + // of bytes per character for the used connection encoding. In our + // case, it's always "utf8mb4" and therefore 4 bytes per character. + if ( + str_contains( $column_info['DATA_TYPE'], 'text' ) + || str_contains( $column_info['DATA_TYPE'], 'char' ) + || 'enum' === $column_info['DATA_TYPE'] + || 'set' === $column_info['DATA_TYPE'] + ) { + // Except for "longtext" - this might be a MySQL bug. + if ( 'longtext' !== $column_info['DATA_TYPE'] ) { + $len = 4 * $len; + } + } + } + + // Flags. + $flags = array(); + if ( 'BLOB' === $native_type ) { + $flags[] = 'blob'; + } + + $column_meta[] = array( + 'native_type' => $native_type, + 'pdo_type' => $pdo_type, + 'flags' => $flags, + 'table' => $meta['table'], + 'name' => $meta['name'], + 'len' => $len, + 'precision' => $precision, + 'sqlite:decl_type' => $meta['sqlite:decl_type'], + ); + } + return $column_meta; + } + /** * Execute a query in SQLite. * @@ -1037,6 +1203,7 @@ private function execute_select_statement( WP_Parser_Node $node ): void { $this->set_results_from_fetched_data( $stmt->fetchAll( $this->pdo_fetch_mode ) ); + $this->last_column_meta_statement = $stmt; } /** @@ -3887,11 +4054,12 @@ private function quote_mysql_utf8_string_literal( string $utf8_literal ): string * Clear the state of the driver. */ private function flush(): void { - $this->last_mysql_query = ''; - $this->last_sqlite_queries = array(); - $this->last_result = null; - $this->last_return_value = null; - $this->is_readonly = false; + $this->last_mysql_query = ''; + $this->last_sqlite_queries = array(); + $this->last_result = null; + $this->last_return_value = null; + $this->last_column_meta_statements = array(); + $this->is_readonly = false; } /** diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php index 3850f88c..1dae2f23 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php @@ -1904,7 +1904,18 @@ private function get_column_lengths( WP_Parser_Node $node, string $data_type, ?s $values = $string_list->get_child_nodes( 'textString' ); $length = 0; foreach ( $values as $value ) { - $length = max( $length, strlen( $this->get_value( $value ) ) ); + if ( 'enum' === $data_type ) { + $length = max( $length, strlen( $this->get_value( $value ) ) ); + } else { + $length += strlen( $this->get_value( $value ) ); + } + } + if ( 'set' === $data_type ) { + if ( 2 === count( $values ) ) { + $length += 1; + } elseif ( count( $values ) > 2 ) { + $length += 2; + } } $max_bytes_per_char = self::CHARSET_MAX_BYTES_MAP[ $charset ] ?? 1; return array( $length, $max_bytes_per_char * $length );