diff --git a/.github/workflows/wp-tests-phpunit-run.js b/.github/workflows/wp-tests-phpunit-run.js index 03ce2697..6d60f8bb 100644 --- a/.github/workflows/wp-tests-phpunit-run.js +++ b/.github/workflows/wp-tests-phpunit-run.js @@ -13,7 +13,6 @@ const path = require( 'path' ); const expectedErrors = [ 'Tests_DB_Charset::test_invalid_characters_in_query', 'Tests_DB_Charset::test_set_charset_changes_the_connection_collation', - 'Tests_DB::test_get_col_info', ]; const expectedFailures = [ diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php index 68ba9242..c60e986e 100644 --- a/tests/WP_SQLite_Driver_Tests.php +++ b/tests/WP_SQLite_Driver_Tests.php @@ -6895,4 +6895,2056 @@ public function testHexadecimalLiterals(): void { $result = $this->assertQuery( "SELECT X'417a'" ); $this->assertEquals( array( (object) array( "X'417a'" => 'Az' ) ), $result ); } + + public function testColumnInfo(): void { + $this->assertQuery( + 'CREATE TABLE t ( + id INT, + name TEXT, + score DOUBLE, + data BLOB, + PRIMARY KEY (id), + UNIQUE KEY (name(64)) + )' + ); + + $this->assertQuery( "INSERT INTO t VALUES (1, 'name', 1.1, B'01101001')" ); + + $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 ); + + $this->assertSame( + array( + 'native_type' => 'LONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array( 'not_null', 'primary_key' ), + 'table' => 't', + 'name' => 'id', + 'len' => 11, + 'precision' => 0, + 'sqlite:decl_type' => 'INT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'id', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 53251 in MySQL. + 'mysqli:type' => 3, + ), + $column_info[0] + ); + + $this->assertSame( + array( + 'native_type' => 'BLOB', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'unique_key', 'blob' ), + 'table' => 't', + 'name' => 'name', + 'len' => 262140, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'name', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, // 16404 in MySQL. + 'mysqli:type' => 252, + ), + $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', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'score', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32768 in MySQL. + 'mysqli:type' => 5, + ), + $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', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'data', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 252, + ), + $column_info[3] + ); + } + + public function testColumnInfoWithConstraints(): void { + $this->assertQuery( + 'CREATE TABLE t ( + id INT PRIMARY KEY, + slug VARCHAR(255) UNIQUE, + parent_id INT, + CONSTRAINT parent_id_fk FOREIGN KEY (parent_id) REFERENCES t (id) + )' + ); + + $this->assertQuery( 'INSERT INTO t VALUES (1, "slug", 1)' ); + + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertEquals( 3, $this->engine->get_last_column_count() ); + + $column_info = $this->engine->get_last_column_meta(); + + $this->assertSame( + array( + array( + 'native_type' => 'LONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array( 'not_null', 'primary_key' ), + 'table' => 't', + 'name' => 'id', + 'len' => 11, + 'precision' => 0, + 'sqlite:decl_type' => 'INT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'id', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 53251 in MySQL. + 'mysqli:type' => 3, + ), + array( + 'native_type' => 'VAR_STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'unique_key' ), + 'table' => 't', + 'name' => 'slug', + 'len' => 1020, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'slug', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, // 16388 in MySQL. + 'mysqli:type' => 253, + ), + array( + // TODO: MySQL seems to automatically create indexes for foreign key columns. + // We should mirror this behavior to both information schema and SQLite. + 'native_type' => 'LONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), // Has "multiple_key" in MySQL. + 'table' => 't', + 'name' => 'parent_id', + 'len' => 11, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'parent_id', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 49160 in MySQL. + 'mysqli:type' => 3, + ), + ), + $column_info + ); + } + + 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( 'INSERT INTO t VALUES (0, 1, 2, 3, 4, 5, 6)' ); + + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertEquals( 7, $this->engine->get_last_column_count() ); + + $column_info = $this->engine->get_last_column_meta(); + + $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', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_bit', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32 in MySQL. + 'mysqli:type' => 16, + ), + array( + 'native_type' => 'TINY', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_bool', + 'len' => 1, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_bool', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32768 in MySQL. + 'mysqli:type' => 1, + ), + array( + 'native_type' => 'TINY', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_tinyint', + 'len' => 4, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_tinyint', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32768 in MySQL. + 'mysqli:type' => 1, + ), + array( + 'native_type' => 'SHORT', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_smallint', + 'len' => 6, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_smallint', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32768 in MySQL. + 'mysqli:type' => 2, + ), + array( + 'native_type' => 'INT24', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_mediumint', + 'len' => 9, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_mediumint', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32768 in MySQL. + 'mysqli:type' => 9, + ), + array( + 'native_type' => 'LONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_int', + 'len' => 11, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_int', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32768 in MySQL. + 'mysqli:type' => 3, + ), + array( + 'native_type' => 'LONGLONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_bigint', + 'len' => 20, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_bigint', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32768 in MySQL. + 'mysqli:type' => 8, + ), + ), + $column_info + ); + } + + public function testColumnInfoForUnsignedIntegerDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE t ( + col_tinyint_unsigned TINYINT UNSIGNED, + col_smallint_unsigned SMALLINT UNSIGNED, + col_mediumint_unsigned MEDIUMINT UNSIGNED, + col_int_unsigned INT UNSIGNED, + col_bigint_unsigned BIGINT UNSIGNED + )' + ); + + $this->assertQuery( 'INSERT INTO t VALUES (1, 2, 3, 4, 5)' ); + + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertEquals( 5, $this->engine->get_last_column_count() ); + + $column_info = $this->engine->get_last_column_meta(); + + $this->assertSame( + array( + array( + 'native_type' => 'TINY', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_tinyint_unsigned', + 'len' => 3, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_tinyint_unsigned', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32800 in MySQL. + 'mysqli:type' => 1, + ), + array( + 'native_type' => 'SHORT', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_smallint_unsigned', + 'len' => 5, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_smallint_unsigned', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32800 in MySQL. + 'mysqli:type' => 2, + ), + array( + 'native_type' => 'INT24', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_mediumint_unsigned', + 'len' => 8, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_mediumint_unsigned', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32800 in MySQL. + 'mysqli:type' => 9, + ), + array( + 'native_type' => 'LONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_int_unsigned', + 'len' => 10, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_int_unsigned', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32800 in MySQL. + 'mysqli:type' => 3, + ), + array( + 'native_type' => 'LONGLONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_bigint_unsigned', + 'len' => 20, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_bigint_unsigned', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32800 in MySQL. + 'mysqli:type' => 8, + ), + ), + $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( 'INSERT INTO t VALUES (1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7)' ); + + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertEquals( 7, $this->engine->get_last_column_count() ); + + $column_info = $this->engine->get_last_column_meta(); + + $this->assertSame( + array( + array( + 'native_type' => 'FLOAT', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_float', + 'len' => 12, + 'precision' => 31, + 'sqlite:decl_type' => 'REAL', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_float', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32768 in MySQL. + 'mysqli:type' => 4, + ), + array( + 'native_type' => 'DOUBLE', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_double', + 'len' => 22, + 'precision' => 31, + 'sqlite:decl_type' => 'REAL', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_double', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32768 in MySQL. + 'mysqli:type' => 5, + ), + array( + 'native_type' => 'DOUBLE', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_real', + 'len' => 22, // PDO reports 22 while MySQLi 12. + 'precision' => 31, + 'sqlite:decl_type' => 'REAL', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_real', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32768 in MySQL. + 'mysqli:type' => 5, // 4 in MySQL. + ), + array( + 'native_type' => 'NEWDECIMAL', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_decimal', + 'len' => 12, + 'precision' => 2, + 'sqlite:decl_type' => 'REAL', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_decimal', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, + 'mysqli:type' => 246, + ), + array( + 'native_type' => 'NEWDECIMAL', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_dec', + 'len' => 12, + 'precision' => 2, + 'sqlite:decl_type' => 'REAL', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_dec', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, + 'mysqli:type' => 246, + ), + array( + 'native_type' => 'NEWDECIMAL', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_fixed', + 'len' => 12, + 'precision' => 2, + 'sqlite:decl_type' => 'REAL', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_fixed', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, + 'mysqli:type' => 246, + ), + array( + 'native_type' => 'NEWDECIMAL', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_numeric', + 'len' => 12, + 'precision' => 2, + 'sqlite:decl_type' => 'REAL', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_numeric', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, + 'mysqli:type' => 246, + ), + ), + $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( 'INSERT INTO t VALUES ("a", "b", "c", "d", "e", "f", "g", "h", "a", "b", "{}")' ); + + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertEquals( 11, $this->engine->get_last_column_count() ); + + $column_info = $this->engine->get_last_column_meta(); + + $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', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_char', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, + 'mysqli:type' => 254, + ), + 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', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_varchar', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, + 'mysqli:type' => 253, + ), + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_nchar', + 'len' => 40, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_nchar', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, + 'mysqli:type' => 254, + ), + 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', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_nvarchar', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, + 'mysqli:type' => 253, + ), + 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', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_tinytext', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, // 16 in MySQL. + 'mysqli:type' => 252, + ), + 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', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_text', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, // 16 in MySQL. + 'mysqli:type' => 252, + ), + 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', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_mediumtext', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, // 16 in MySQL. + 'mysqli:type' => 252, + ), + 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', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_longtext', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, // 16 in MySQL. + 'mysqli:type' => 252, + ), + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_enum', + 'len' => 4, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_enum', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, // 256 in MySQL. + 'mysqli:type' => 254, + ), + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_set', + 'len' => 20, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_set', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, // 2048 in MySQL. + 'mysqli:type' => 254, + ), + 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', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_json', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, // 63 in MySQL. + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 245, + ), + ), + $column_info + ); + } + + public function testColumnInfoForDateAndTimeDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE t ( + col_date DATE, + col_time TIME, + col_datetime DATETIME, + col_timestamp TIMESTAMP, + col_year YEAR + )' + ); + + $this->assertQuery( 'INSERT INTO t VALUES ("2024-01-01", "12:00:00", "2024-01-01 12:00:00", "2024-01-01 12:00:00", 2024)' ); + + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertEquals( 5, $this->engine->get_last_column_count() ); + + $column_info = $this->engine->get_last_column_meta(); + + $this->assertSame( + array( + array( + 'native_type' => 'DATE', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_date', + 'len' => 10, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_date', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 128 in MySQL. + 'mysqli:type' => 10, + ), + array( + 'native_type' => 'TIME', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_time', + 'len' => 10, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_time', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 128 in MySQL. + 'mysqli:type' => 11, + ), + array( + 'native_type' => 'DATETIME', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_datetime', + 'len' => 19, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_datetime', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 128 in MySQL. + 'mysqli:type' => 12, + ), + array( + 'native_type' => 'TIMESTAMP', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_timestamp', + 'len' => 19, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_timestamp', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 128 in MySQL. + 'mysqli:type' => 7, + ), + array( + 'native_type' => 'YEAR', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_year', + 'len' => 4, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_year', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32864 in MySQL. + 'mysqli:type' => 13, + ), + ), + $column_info + ); + } + + public function testColumnInfoForBinaryDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE t ( + col_binary BINARY(10), + col_varbinary VARBINARY(10), + col_tinyblob TINYBLOB, + col_blob BLOB, + col_mediumblob MEDIUMBLOB, + col_longblob LONGBLOB + )' + ); + + $this->assertQuery( "INSERT INTO t VALUES (B'01000001', B'01101001', B'10101010', B'01010101', B'10000000', B'11111111')" ); + + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertEquals( 6, $this->engine->get_last_column_count() ); + + $column_info = $this->engine->get_last_column_meta(); + + $this->assertSame( + array( + array( + 'native_type' => 'BLOB', // STRING in MySQL. + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), // No flags in MySQL. + 'table' => 't', + 'name' => 'col_binary', + 'len' => 10, + 'precision' => 0, + 'sqlite:decl_type' => 'BLOB', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_binary', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 128 in MySQL. + 'mysqli:type' => 254, + ), + array( + 'native_type' => 'BLOB', // VAR_STRING in MySQL. + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), // No flags in MySQL. + 'table' => 't', + 'name' => 'col_varbinary', + 'len' => 10, + 'precision' => 0, + 'sqlite:decl_type' => 'BLOB', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_varbinary', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 128 in MySQL. + 'mysqli:type' => 253, + ), + array( + 'native_type' => 'BLOB', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_tinyblob', + 'len' => 255, + 'precision' => 0, + 'sqlite:decl_type' => 'BLOB', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_tinyblob', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 252, + ), + array( + 'native_type' => 'BLOB', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_blob', + 'len' => 65535, + 'precision' => 0, + 'sqlite:decl_type' => 'BLOB', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_blob', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 252, + ), + array( + 'native_type' => 'BLOB', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_mediumblob', + 'len' => 16777215, + 'precision' => 0, + 'sqlite:decl_type' => 'BLOB', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_mediumblob', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 252, + ), + array( + 'native_type' => 'BLOB', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_longblob', + 'len' => 4294967295, + 'precision' => 0, + 'sqlite:decl_type' => 'BLOB', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_longblob', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 252, + ), + ), + $column_info + ); + } + + public function testColumnInfoForSpatialDataTypes(): void { + $this->assertQuery( + 'CREATE TABLE t ( + col_geometry GEOMETRY, + col_point POINT, + col_linestring LINESTRING, + col_polygon POLYGON, + col_multipoint MULTIPOINT, + col_multilinestring MULTILINESTRING, + col_multipolygon MULTIPOLYGON, + col_geomcollection GEOMCOLLECTION, + col_geometrycollection GEOMETRYCOLLECTION + )' + ); + + $this->assertQuery( + "INSERT INTO t VALUES ( + 'POINT(1 1)', + 'POINT(1 1)', + 'LINESTRING(0 0, 1 1)', + 'POLYGON((0 0, 1 0, 0 1, 0 0))', + 'MULTIPOINT(1 1, 2 2)', + 'MULTILINESTRING((0 0, 1 1), (2 2, 3 3))', + 'MULTIPOLYGON(((0 0, 1 0, 0 1, 0 0)))', + 'GEOMCOLLECTION(POINT(1 1), LINESTRING(0 0, 1 1))', + 'GEOMETRYCOLLECTION(POINT(1 1), LINESTRING(0 0, 1 1))' + )" + ); + + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertEquals( 9, $this->engine->get_last_column_count() ); + + $column_info = $this->engine->get_last_column_meta(); + + $this->assertSame( + array( + array( + 'native_type' => 'GEOMETRY', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_geometry', + 'len' => 4294967295, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_geometry', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 255, + ), + array( + 'native_type' => 'GEOMETRY', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_point', + 'len' => 4294967295, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_point', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 255, + ), + array( + 'native_type' => 'GEOMETRY', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_linestring', + 'len' => 4294967295, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_linestring', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 255, + ), + array( + 'native_type' => 'GEOMETRY', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_polygon', + 'len' => 4294967295, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_polygon', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 255, + ), + array( + 'native_type' => 'GEOMETRY', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_multipoint', + 'len' => 4294967295, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_multipoint', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 255, + ), + array( + 'native_type' => 'GEOMETRY', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_multilinestring', + 'len' => 4294967295, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_multilinestring', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 255, + ), + array( + 'native_type' => 'GEOMETRY', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_multipolygon', + 'len' => 4294967295, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_multipolygon', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 255, + ), + array( + 'native_type' => 'GEOMETRY', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_geomcollection', + 'len' => 4294967295, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_geomcollection', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 255, + ), + array( + 'native_type' => 'GEOMETRY', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_geometrycollection', + 'len' => 4294967295, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_geometrycollection', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 255, + ), + ), + $column_info + ); + } + + public function testColumnInfoForExpressions(): void { + $this->assertQuery( 'CREATE TABLE t (id INT)' ); + $this->assertQuery( 'INSERT INTO t VALUES (1)' ); + $this->assertQuery( + "SELECT + NULL AS col_expr_1, + TRUE AS col_expr_2, + FALSE AS col_expr_3, + 1 AS col_expr_4, + (1 + 1) AS col_expr_5, + 'abc' AS col_expr_6, + COUNT(*) AS col_expr_7, + SUM(id) AS col_expr_8, + CONCAT('a', 'b') AS col_expr_9, + YEAR('2025-01-01') AS col_expr_10, + CAST('2024-01-01' AS DATE) AS col_expr_11, + CAST(X'68656C6C6F' AS BINARY) AS col_expr_12, + CAST('123' AS CHAR) AS col_expr_13, + CAST(42 AS SIGNED) AS col_expr_14, + COALESCE(NULL, 'fallback') AS col_expr_15, + CASE WHEN id > 5 THEN 'yes' ELSE 'no' END AS col_expr_16, + CASE WHEN id < 5 THEN 'string' ELSE 123 END AS col_expr_17, + ABS(-7) AS col_expr_18, + RAND() AS col_expr_19, + (SELECT 1) AS col_expr_20 + FROM t" + ); + $this->assertEquals( 20, $this->engine->get_last_column_count() ); + + $column_info = $this->engine->get_last_column_meta(); + + $this->assertSame( + array( + array( + 'native_type' => 'NULL', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => '', + 'name' => 'col_expr_1', + 'len' => 0, + 'precision' => 0, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_1', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32896 in MySQL. + 'mysqli:type' => 6, + ), + array( + 'native_type' => 'LONGLONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_2', + 'len' => 21, // 1 in MySQL. + 'precision' => 0, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_2', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32897 in MySQL. + 'mysqli:type' => 8, + ), + array( + 'native_type' => 'LONGLONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_3', + 'len' => 21, // 1 in MySQL. + 'precision' => 0, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_3', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32897 in MySQL. + 'mysqli:type' => 8, + ), + array( + 'native_type' => 'LONGLONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_4', + 'len' => 21, // 2 in MySQL. + 'precision' => 0, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_4', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32897 in MySQL. + 'mysqli:type' => 8, + ), + array( + 'native_type' => 'LONGLONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_5', + 'len' => 21, // 3 in MySQL. + 'precision' => 0, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_5', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32897 in MySQL. + 'mysqli:type' => 8, + ), + array( + 'native_type' => 'VAR_STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_6', + 'len' => 65535, // 12 in MySQL. + 'precision' => 31, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_6', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, // 1 in MySQL. + 'mysqli:type' => 253, + ), + array( + 'native_type' => 'LONGLONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_7', + 'len' => 21, + 'precision' => 0, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_7', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32769 in MySQL. + 'mysqli:type' => 8, + ), + array( + 'native_type' => 'LONGLONG', // NEWDECIMAL in MySQL. + 'pdo_type' => PDO::PARAM_INT, // PARAM_STR in MySQL. + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_8', + 'len' => 21, // 33 in MySQL. + 'precision' => 0, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_8', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, + 'mysqli:type' => 8, // 246 in MySQL. + ), + array( + 'native_type' => 'VAR_STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_9', + 'len' => 65535, // 8 in MySQL. + 'precision' => 31, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_9', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, + 'mysqli:type' => 253, + ), + array( + 'native_type' => 'LONGLONG', // "YEAR" in MySQL. + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array( 'not_null' ), // Empty in MySQL. + 'table' => '', + 'name' => 'col_expr_10', + 'len' => 21, // 4 in MySQL. + 'precision' => 0, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_10', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32928 in MySQL. + 'mysqli:type' => 8, // 13 in MySQL. + ), + array( + // "CAST('2024-01-01' AS DATE)" seems to behave differently in SQLite. + 'native_type' => 'LONGLONG', // "DATE" in MySQL. + 'pdo_type' => PDO::PARAM_INT, // PARAM_STR in MySQL. + 'flags' => array( 'not_null' ), // Empty in MySQL. + 'table' => '', + 'name' => 'col_expr_11', + 'len' => 21, // 10 in MySQL. + 'precision' => 0, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_11', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 128 in MySQL. + 'mysqli:type' => 8, // 10 in MySQL. + ), + array( + 'native_type' => 'VAR_STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_12', + 'len' => 65535, // 5 in MySQL. + 'precision' => 31, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_12', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, // 63 in MySQL. + 'mysqli:flags' => 0, // 128 in MySQL. + 'mysqli:type' => 253, + ), + array( + 'native_type' => 'VAR_STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_13', + 'len' => 65535, // 12 in MySQL. + 'precision' => 31, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_13', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, + 'mysqli:type' => 253, + ), + array( + 'native_type' => 'LONGLONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_14', + 'len' => 21, + 'precision' => 0, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_14', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32897 in MySQL. + 'mysqli:type' => 8, + ), + array( + 'native_type' => 'VAR_STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_15', + 'len' => 65535, // 32 in MySQL. + 'precision' => 31, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_15', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, // 1 in MySQL. + 'mysqli:type' => 253, + ), + array( + 'native_type' => 'VAR_STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_16', + 'len' => 65535, // 12 in MySQL. + 'precision' => 31, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_16', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, // 1 in MySQL. + 'mysqli:type' => 253, + ), + array( + 'native_type' => 'VAR_STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_17', + 'len' => 65535, // 24 in MySQL. + 'precision' => 31, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_17', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, // 1 in MySQL. + 'mysqli:type' => 253, + ), + array( + 'native_type' => 'LONGLONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_18', + 'len' => 21, // 2 in MySQL. + 'precision' => 0, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_18', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32897 in MySQL. + 'mysqli:type' => 8, + ), + array( + // TODO: Fix custom "RAND()" function to behave like in MySQL. + 'native_type' => 'LONGLONG', // DOUBLE in MySQL. + 'pdo_type' => PDO::PARAM_INT, // PARAM_STR in MySQL. + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_19', + 'len' => 21, // 23 in MySQL. + 'precision' => 0, // 31 in MySQL. + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_19', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32769 in MySQL. + 'mysqli:type' => 8, // 5 in MySQL. + ), + array( + 'native_type' => 'LONGLONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_20', + 'len' => 21, // 2 in MySQL. + 'precision' => 0, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_20', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32897 in MySQL. + 'mysqli:type' => 8, + ), + ), + $column_info + ); + } + + public function testColumnInfoWithZeroRows(): void { + $this->assertQuery( + 'CREATE TABLE t ( + col_int INT, + col_float FLOAT, + col_char CHAR(10), + col_varchar VARCHAR(10), + col_text TEXT, + col_json JSON, + col_binary BINARY(10), + col_varbinary VARBINARY(10), + col_blob BLOB, + col_date DATE, + col_timestamp TIMESTAMP, + col_geometry GEOMETRY + )' + ); + + $this->assertQuery( + "SELECT + *, + COUNT(*) AS col_expr_1, + SUM(col_int) AS col_expr_2, + CASE WHEN col_int > 5 THEN 'yes' ELSE 'no' END AS col_expr_3, + CASE WHEN col_int < 5 THEN 'string' ELSE 123 END AS col_expr_4 + FROM t" + ); + $this->assertEquals( 16, $this->engine->get_last_column_count() ); + + $column_info = $this->engine->get_last_column_meta(); + $this->assertCount( 16, $column_info ); + + $this->assertSame( + array( + array( + 'native_type' => 'LONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_int', + 'len' => 11, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_int', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32768 in MySQL. + 'mysqli:type' => 3, + ), + array( + 'native_type' => 'FLOAT', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_float', + 'len' => 12, + 'precision' => 31, + 'sqlite:decl_type' => 'REAL', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_float', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 32768 in MySQL. + 'mysqli:type' => 4, + ), + array( + 'native_type' => 'STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_char', + 'len' => 40, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_char', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, + 'mysqli:type' => 254, + ), + 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', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_varchar', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, + 'mysqli:type' => 253, + ), + 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', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_text', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, + 'mysqli:flags' => 0, // 16 in MySQL. + 'mysqli:type' => 252, + ), + array( + 'native_type' => 'BLOB', // Missing in MySQL. + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_json', + 'len' => 4294967295, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_json', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, // 63 in MySQL. + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 245, + ), + array( + 'native_type' => 'BLOB', // STRING in MySQL. + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_binary', + 'len' => 10, + 'precision' => 0, + 'sqlite:decl_type' => 'BLOB', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_binary', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 128 in MySQL. + 'mysqli:type' => 254, + ), + array( + 'native_type' => 'BLOB', // VAR_STRING in MySQL. + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_varbinary', + 'len' => 10, + 'precision' => 0, + 'sqlite:decl_type' => 'BLOB', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_varbinary', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 128 in MySQL. + 'mysqli:type' => 253, + ), + array( + 'native_type' => 'BLOB', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_blob', + 'len' => 65535, + 'precision' => 0, + 'sqlite:decl_type' => 'BLOB', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_blob', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 252, + ), + array( + 'native_type' => 'DATE', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_date', + 'len' => 10, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_date', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 128 in MySQL. + 'mysqli:type' => 10, + ), + array( + 'native_type' => 'TIMESTAMP', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => 't', + 'name' => 'col_timestamp', + 'len' => 19, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_timestamp', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 128 in MySQL. + 'mysqli:type' => 7, + ), + array( + 'native_type' => 'GEOMETRY', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'blob' ), + 'table' => 't', + 'name' => 'col_geometry', + 'len' => 4294967295, + 'precision' => 0, + 'sqlite:decl_type' => 'TEXT', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_geometry', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 144 in MySQL. + 'mysqli:type' => 255, + ), + array( + 'native_type' => 'LONGLONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_1', + 'len' => 21, + 'precision' => 0, // 32897 in MySQL. + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_1', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, + 'mysqli:type' => 8, + ), + array( + // For "SUM(*)" without rows, SQLite fails to provide a type. + 'native_type' => 'NULL', // NEWDECIMAL in MySQL. + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array(), + 'table' => '', + 'name' => 'col_expr_2', + 'len' => 0, // 33 in MySQL. + 'precision' => 0, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_2', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, // 128 in MySQL. + 'mysqli:type' => 6, // 246 in MySQL. + ), + array( + 'native_type' => 'VAR_STRING', + 'pdo_type' => PDO::PARAM_STR, + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_3', + 'len' => 65535, // 12 in MySQL. + 'precision' => 31, + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_3', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 255, // 63 in MySQL. + 'mysqli:flags' => 0, + 'mysqli:type' => 253, + ), + array( + 'native_type' => 'LONGLONG', // VAR_STRING in MySQL. + 'pdo_type' => PDO::PARAM_INT, // PARAM_STR in MySQL. + 'flags' => array( 'not_null' ), + 'table' => '', + 'name' => 'col_expr_4', + 'len' => 21, // 24 in MySQL. + 'precision' => 0, // 31 in MySQL. + 'sqlite:decl_type' => '', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'col_expr_4', + 'mysqli:orgtable' => '', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, // 255 in MySQL. + 'mysqli:flags' => 0, // 1 in MySQL. + 'mysqli:type' => 8, // 253 in MySQL. + ), + ), + $column_info + ); + } + + public function testColumnInfoWithZeroRowsPhpBug(): void { + if ( PHP_VERSION_ID < 70300 ) { + $this->markTestSkipped( 'Skipping due to PHP bug (#79664)' ); + } + + $this->assertQuery( 'CREATE TABLE t ( id INT )' ); + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertEquals( 1, $this->engine->get_last_column_count() ); + $column_info = $this->engine->get_last_column_meta(); + $this->assertCount( 1, $column_info ); + $this->assertSame( + array( + 'native_type' => 'LONG', + 'pdo_type' => PDO::PARAM_INT, + 'flags' => array(), + 'table' => 't', + 'name' => 'id', + 'len' => 11, + 'precision' => 0, + 'sqlite:decl_type' => 'INTEGER', + + // Additional MySQLi metadata. + 'mysqli:orgname' => 'id', + 'mysqli:orgtable' => 't', + 'mysqli:db' => 'wp', + 'mysqli:charsetnr' => 63, + 'mysqli:flags' => 0, + 'mysqli:type' => 3, + ), + $column_info[0] + ); + } } diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index 2945b7e5..dd1a8d39 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -294,6 +294,92 @@ class WP_SQLite_Driver { 'geometrycollection' => null, ); + /** + * A map of MySQL column data types to native types in MySQL column meta. + * + * This maps normalized MySQL column data types (as per information schema) + * to MySQL "PDOStatement::getColumnMeta()" data types in the "native_type" + * field, as well as the "len" and "precision" fields, where applicable: + * + * => array( , , , ) + * + * This is used to compute the column metadata from the information schema. + */ + const COLUMN_INFO_MYSQL_TO_NATIVE_TYPES_MAP = array( + // Numeric data types: + 'bit' => array( 'BIT', 16, 1, 0 ), + 'tinyint' => array( 'TINY', 1, 4, 0 ), + 'smallint' => array( 'SHORT', 2, 6, 0 ), + 'mediumint' => array( 'INT24', 9, 9, 0 ), + 'int' => array( 'LONG', 3, 11, 0 ), + 'bigint' => array( 'LONGLONG', 8, 20, 0 ), + 'float' => array( 'FLOAT', 4, 12, 31 ), + 'double' => array( 'DOUBLE', 5, 22, 31 ), + 'decimal' => array( 'NEWDECIMAL', 246, null, null ), + + // String data types: + 'char' => array( 'STRING', 254, null, 0 ), + 'varchar' => array( 'VAR_STRING', 253, null, 0 ), + 'tinytext' => array( 'BLOB', 252, null, 0 ), + 'text' => array( 'BLOB', 252, null, 0 ), + 'mediumtext' => array( 'BLOB', 252, null, 0 ), + 'longtext' => array( 'BLOB', 252, null, 0 ), + 'enum' => array( 'STRING', 254, null, 0 ), + 'set' => array( 'STRING', 254, null, 0 ), + 'json' => array( 'BLOB', 245, 4294967295, 0 ), + + // Date and time data types: + 'date' => array( 'DATE', 10, 10, 0 ), + 'time' => array( 'TIME', 11, 10, 0 ), + 'datetime' => array( 'DATETIME', 12, 19, 0 ), + 'timestamp' => array( 'TIMESTAMP', 7, 19, 0 ), + 'year' => array( 'YEAR', 13, 4, 0 ), + + // Binary data types: + 'binary' => array( 'BLOB', 254, null, 0 ), + 'varbinary' => array( 'BLOB', 253, null, 0 ), + 'tinyblob' => array( 'BLOB', 252, null, 0 ), + 'blob' => array( 'BLOB', 252, null, 0 ), + 'mediumblob' => array( 'BLOB', 252, null, 0 ), + 'longblob' => array( 'BLOB', 252, null, 0 ), + + // Spatial data types: + 'geometry' => array( 'GEOMETRY', 255, 4294967295, 0 ), + 'point' => array( 'GEOMETRY', 255, 4294967295, 0 ), + 'linestring' => array( 'GEOMETRY', 255, 4294967295, 0 ), + 'polygon' => array( 'GEOMETRY', 255, 4294967295, 0 ), + 'multipoint' => array( 'GEOMETRY', 255, 4294967295, 0 ), + 'multilinestring' => array( 'GEOMETRY', 255, 4294967295, 0 ), + 'multipolygon' => array( 'GEOMETRY', 255, 4294967295, 0 ), + 'geomcollection' => array( 'GEOMETRY', 255, 4294967295, 0 ), + ); + + /** + * A map of SQLite column definition data types and SQLite column meta data + * types to native types in MySQL column meta. + * + * This maps both SQLite column definition data types and SQLite column meta + * data types (as per "PDOStatement::getColumnMeta()") to MySQL column meta + * "native_type" field, as per "PDOStatement::getColumnMeta()", as well as + * the "len" and "precision" fields, where applicable: + * + * => array( , , , ) + * => array( , , , ) + * + * This is used to compute the MySQL column metadata for non-column fields + * that have no records in the information schema (i.e., expressions). + */ + const COLUMN_INFO_SQLITE_TO_NATIVE_TYPES_MAP = array( + 'NULL' => array( 'NULL', 6, 0, 0 ), + 'INT' => array( 'LONGLONG', 8, 21, 0 ), + 'INTEGER' => array( 'LONGLONG', 8, 21, 0 ), + 'STRING' => array( 'VAR_STRING', 253, 65535, 31 ), + 'TEXT' => array( 'BLOB', 252, null, 0 ), + 'REAL' => array( 'DOUBLE', 5, 22, 31 ), + 'DOUBLE' => array( 'DOUBLE', 5, 23, 31 ), + 'BLOB' => array( 'BLOB', 252, null, 0 ), + ); + /** * The SQLite engine version. * @@ -378,6 +464,13 @@ class WP_SQLite_Driver { */ private $last_return_value; + /** + * SQLite column metadata for the last emulated query. + * + * @var array + */ + private $last_column_meta = array(); + /** * Number of rows found by the last SQL_CALC_FOUND_ROW query. * @@ -754,6 +847,176 @@ 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 { + return count( $this->last_column_meta ); + } + + /** + * Get column metadata for results of the last emulated query. + * + * @return array + */ + public function get_last_column_meta(): array { + // Build the column metadata as per "PDOStatement::getColumnMeta()". + $column_meta = array(); + foreach ( $this->last_column_meta as $meta ) { + $table = $meta['table'] ?? null; + $name = $meta['name']; + $type = strtoupper( $meta['sqlite:decl_type'] ?? $meta['native_type'] ?? '' ); + + // When table is known, we can get data from the information schema. + $column_info = null; + if ( null !== $table ) { + $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 + IS_NULLABLE, + DATA_TYPE, + COLUMN_TYPE, + COLUMN_KEY, + 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 ); + + if ( false === $column_info ) { + $column_info = null; + } + } + + // If we have information schema data, we can use it. + if ( null !== $column_info ) { + $type_info = self::COLUMN_INFO_MYSQL_TO_NATIVE_TYPES_MAP[ $column_info['DATA_TYPE'] ] ?? null; + if ( null === $type_info ) { + $type_info = self::COLUMN_INFO_SQLITE_TO_NATIVE_TYPES_MAP[ $type ] ?? null; + } + $native_type = $type_info[0]; + $mysqli_type = $type_info[1]; + $len = $type_info[2]; + $precision = $type_info[3]; + + 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 ( + str_contains( $column_info['COLUMN_TYPE'], 'unsigned' ) + && ! str_contains( $column_info['COLUMN_TYPE'], 'bigint' ) + ) { + $len -= 1; + } + + // 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 ( 'NO' === $column_info['IS_NULLABLE'] ) { + $flags[] = 'not_null'; + } + if ( 'PRI' === $column_info['COLUMN_KEY'] ) { + $flags[] = 'primary_key'; + } elseif ( 'UNI' === $column_info['COLUMN_KEY'] ) { + $flags[] = 'unique_key'; + } elseif ( 'MUL' === $column_info['COLUMN_KEY'] ) { + $flags[] = 'multiple_key'; + } + } else { + $type_info = self::COLUMN_INFO_SQLITE_TO_NATIVE_TYPES_MAP[ $type ]; + $native_type = $type_info[0]; + $mysqli_type = $type_info[1]; + $len = $type_info[2] ?? 0; + $precision = $type_info[3]; + + // Flags. + $flags = array(); + if ( 'NULL' !== $type ) { + $flags[] = 'not_null'; + } + } + + if ( 'BLOB' === $native_type || 'GEOMETRY' === $native_type ) { + $flags[] = 'blob'; + } + + // PDO type. + if ( 'INT' === $type || 'INTEGER' === $type ) { + $pdo_type = PDO::PARAM_INT; + } else { + $pdo_type = PDO::PARAM_STR; + } + + // MySQLi charset number. + $is_string = 'STRING' === $type || 'TEXT' === $type; + $is_binary = 'BLOB' === $type || 'GEOMETRY' === $native_type; + $is_datetime = str_contains( $native_type, 'DATE' ) || str_contains( $native_type, 'TIME' ) || 'YEAR' === $native_type; + if ( $is_string && ! $is_binary && ! $is_datetime ) { + $mysqli_charsetnr = 255; // utf8mb4_0900_ai_ci + } else { + $mysqli_charsetnr = 63; // binary + } + + $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'] ?? '', + + /* + * The MySQLi PHP extension exposes more MySQL column metadata than PDO. + * We'll add the data here for use cases such as "wpdb::get_col_info()". + */ + 'mysqli:orgname' => $meta['name'], // TODO: Use correct original name when alias is used. + 'mysqli:orgtable' => $meta['table'] ?? '', // TODO: Use correct original name when table alias is used. + 'mysqli:db' => $this->db_name, // TODO: Use correct DB for queries to information schema. + 'mysqli:charsetnr' => $mysqli_charsetnr, + 'mysqli:flags' => 0, // TODO: We can compute correct MySQL flags. + 'mysqli:type' => $mysqli_type, + ); + } + return $column_meta; + } + /** * Execute a query in SQLite. * @@ -1197,6 +1460,35 @@ private function execute_select_statement( WP_Parser_Node $node ): void { // Execute the query. $stmt = $this->execute_sqlite_query( $query ); + + // 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->set_results_from_fetched_data( $stmt->fetchAll( $this->pdo_fetch_mode ) ); @@ -4986,6 +5278,7 @@ private function flush(): void { $this->last_sqlite_queries = array(); $this->last_result = null; $this->last_return_value = null; + $this->last_column_meta = 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 404f03ed..cd8e07cf 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 @@ -2298,7 +2298,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 ); diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php index d8eca0bb..1bc00aaa 100644 --- a/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/wp-includes/sqlite/class-wp-sqlite-db.php @@ -567,7 +567,28 @@ protected function load_col_info() { if ( $this->col_info ) { return; } - $this->col_info = $this->dbh->get_columns(); + if ( $this->dbh instanceof WP_SQLite_Driver ) { + $this->col_info = array(); + foreach ( $this->dbh->get_last_column_meta() as $column ) { + $this->col_info[] = (object) array( + 'name' => $column['name'], + 'orgname' => $column['mysqli:orgname'], + 'table' => $column['table'], + 'orgtable' => $column['mysqli:orgtable'], + 'def' => '', // Unused, always ''. + 'db' => $column['mysqli:db'], + 'catalog' => 'def', // Unused, always 'def'. + 'max_length' => 0, // As of PHP 8.1, this is always 0. + 'length' => $column['len'], + 'charsetnr' => $column['mysqli:charsetnr'], + 'flags' => $column['mysqli:flags'], + 'type' => $column['mysqli:type'], + 'decimals' => $column['precision'], + ); + } + } else { + $this->col_info = $this->dbh->get_columns(); + } } /**