diff --git a/tests/WP_PDO_MySQL_On_SQLite_PDO_API_Tests.php b/tests/WP_PDO_MySQL_On_SQLite_PDO_API_Tests.php
index c6ac2017..d9b474fb 100644
--- a/tests/WP_PDO_MySQL_On_SQLite_PDO_API_Tests.php
+++ b/tests/WP_PDO_MySQL_On_SQLite_PDO_API_Tests.php
@@ -7,8 +7,234 @@ class WP_PDO_MySQL_On_SQLite_PDO_API_Tests extends TestCase {
private $driver;
public function setUp(): void {
- $connection = new WP_SQLite_Connection( array( 'path' => ':memory:' ) );
- $this->driver = new WP_PDO_MySQL_On_SQLite( $connection, 'wp' );
+ $this->driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=wp;' );
+
+ // Run all tests with stringified fetch mode results, so we can use
+ // assertions that are consistent across all tested PHP versions.
+ // The "PDO::ATTR_STRINGIFY_FETCHES" mode is tested separately.
+ $this->driver->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
+ }
+
+ public function test_connection(): void {
+ $driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=WordPress;' );
+ $this->assertInstanceOf( PDO::class, $driver );
+ }
+
+ public function test_dsn_parsing(): void {
+ // Standard DSN.
+ $driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=wp' );
+ $this->assertSame( 'wp', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );
+
+ // DSN with trailing semicolon.
+ $driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=wp;' );
+ $this->assertSame( 'wp', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );
+
+ // DSN with whitespace before argument names.
+ $driver = new WP_PDO_MySQL_On_SQLite( "mysql-on-sqlite: path=:memory:; \n\r\t\v\fdbname=wp" );
+ $this->assertSame( 'wp', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );
+
+ // DSN with whitespace in the database name.
+ $driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname= w p ' );
+ $this->assertSame( ' w p ', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );
+
+ // DSN with semicolon in the database name.
+ $driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=wp;dbname=w;;p;' );
+ $this->assertSame( 'w;p', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );
+
+ // DSN with semicolon in the database name and a terminating semicolon.
+ $driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=w;;;p' );
+ $this->assertSame( 'w;', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );
+
+ // DSN with two semicolons in the database name.
+ $driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=w;;;;p' );
+ $this->assertSame( 'w;;p', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );
+
+ // DSN with a "\0" byte (always terminates the DSN string).
+ $driver = new WP_PDO_MySQL_On_SQLite( "mysql-on-sqlite:path=:memory:;dbname=w\0p;" );
+ $this->assertSame( 'w', $driver->query( 'SELECT DATABASE()' )->fetch()[0] );
+ }
+
+ public function test_query(): void {
+ $result = $this->driver->query( "SELECT 1, 'abc'" );
+ $this->assertInstanceOf( PDOStatement::class, $result );
+ if ( PHP_VERSION_ID < 80000 ) {
+ $this->assertSame(
+ array(
+ 1 => '1',
+ 2 => '1',
+ 'abc' => 'abc',
+ 3 => 'abc',
+ ),
+ $result->fetch()
+ );
+ } else {
+ $this->assertSame(
+ array(
+ 1 => '1',
+ 0 => '1',
+ 'abc' => 'abc',
+ ),
+ $result->fetch()
+ );
+ }
+ }
+
+ /**
+ * @dataProvider data_pdo_fetch_methods
+ */
+ public function test_query_with_fetch_mode( $query, $mode, $expected ): void {
+ $stmt = $this->driver->query( $query, $mode );
+ $result = $stmt->fetch();
+
+ if ( is_object( $expected ) ) {
+ $this->assertInstanceOf( get_class( $expected ), $result );
+ $this->assertSame( (array) $expected, (array) $result );
+ } elseif ( PDO::FETCH_NAMED === $mode ) {
+ // PDO::FETCH_NAMED returns all array keys as strings, even numeric
+ // ones. This is not possible in plain PHP and might be a PDO bug.
+ $this->assertSame( array_map( 'strval', array_keys( $expected ) ), array_keys( $result ) );
+ $this->assertSame( array_values( $expected ), array_values( $result ) );
+ } else {
+ $this->assertSame( $expected, $result );
+ }
+
+ $this->assertFalse( $stmt->fetch() );
+ }
+
+ public function test_query_fetch_mode_not_set(): void {
+ $result = $this->driver->query( 'SELECT 1' );
+ if ( PHP_VERSION_ID < 80000 ) {
+ $this->assertSame(
+ array(
+ 1 => '1',
+ 2 => '1',
+ ),
+ $result->fetch()
+ );
+ } else {
+ $this->assertSame(
+ array(
+ 1 => '1',
+ 0 => '1',
+ ),
+ $result->fetch()
+ );
+ }
+ $this->assertFalse( $result->fetch() );
+ }
+
+ public function test_query_fetch_mode_invalid_arg_count(): void {
+ $this->expectException( ArgumentCountError::class );
+ $this->expectExceptionMessage( 'PDO::query() expects exactly 2 arguments for the fetch mode provided, 3 given' );
+ $this->driver->query( 'SELECT 1', PDO::FETCH_ASSOC, 0 );
+ }
+
+ public function test_query_fetch_default_mode_allow_any_args(): void {
+ if ( PHP_VERSION_ID < 80100 ) {
+ // On PHP < 8.1, fetch mode value of NULL is not allowed.
+ $result = @$this->driver->query( 'SELECT 1', null, 1, 2, 'abc', array(), true ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+ $this->assertFalse( $result );
+ $this->assertSame( 'PDO::query(): SQLSTATE[HY000]: General error: mode must be an integer', error_get_last()['message'] );
+ return;
+ }
+
+ // On PHP >= 8.1, NULL fetch mode is allowed to use the default fetch mode.
+ // In such cases, any additional arguments are ignored and not validated.
+ $expected_result = array(
+ array(
+ 1 => '1',
+ 0 => '1',
+ ),
+ );
+
+ $result = $this->driver->query( 'SELECT 1' );
+ $this->assertSame( $expected_result, $result->fetchAll() );
+
+ $result = $this->driver->query( 'SELECT 1', null );
+ $this->assertSame( $expected_result, $result->fetchAll() );
+
+ $result = $this->driver->query( 'SELECT 1', null, 1 );
+ $this->assertSame( $expected_result, $result->fetchAll() );
+
+ $result = $this->driver->query( 'SELECT 1', null, 'abc' );
+ $this->assertSame( $expected_result, $result->fetchAll() );
+
+ $result = $this->driver->query( 'SELECT 1', null, 1, 2, 'abc', array(), true );
+ $this->assertSame( $expected_result, $result->fetchAll() );
+ }
+
+ public function test_query_fetch_class_not_enough_args(): void {
+ $this->expectException( ArgumentCountError::class );
+ $this->expectExceptionMessage( 'PDO::query() expects at least 3 arguments for the fetch mode provided, 2 given' );
+ $this->driver->query( 'SELECT 1', PDO::FETCH_CLASS );
+ }
+
+ public function test_query_fetch_class_too_many_args(): void {
+ $this->expectException( ArgumentCountError::class );
+ $this->expectExceptionMessage( 'PDO::query() expects at most 4 arguments for the fetch mode provided, 5 given' );
+ $this->driver->query( 'SELECT 1', PDO::FETCH_CLASS, '\stdClass', array(), array() );
+ }
+
+ public function test_query_fetch_class_invalid_class_type(): void {
+ $this->expectException( TypeError::class );
+ $this->expectExceptionMessage( 'PDO::query(): Argument #3 must be of type string, int given' );
+ $this->driver->query( 'SELECT 1', PDO::FETCH_CLASS, 1 );
+ }
+
+ public function test_query_fetch_class_invalid_class_name(): void {
+ $this->expectException( TypeError::class );
+ $this->expectExceptionMessage( 'PDO::query(): Argument #3 must be a valid class' );
+ $this->driver->query( 'SELECT 1', PDO::FETCH_CLASS, 'non-existent-class' );
+ }
+
+ public function test_query_fetch_class_invalid_constructor_args_type(): void {
+ $this->expectException( TypeError::class );
+ $this->expectExceptionMessage( 'PDO::query(): Argument #4 must be of type ?array, int given' );
+ $this->driver->query( 'SELECT 1', PDO::FETCH_CLASS, 'stdClass', 1 );
+ }
+
+ public function test_query_fetch_into_invalid_arg_count(): void {
+ $this->expectException( ArgumentCountError::class );
+ $this->expectExceptionMessage( 'PDO::query() expects exactly 3 arguments for the fetch mode provided, 2 given' );
+ $this->driver->query( 'SELECT 1', PDO::FETCH_INTO );
+ }
+
+ public function test_query_fetch_into_invalid_object_type(): void {
+ $this->expectException( TypeError::class );
+ $this->expectExceptionMessage( 'PDO::query(): Argument #3 must be of type object, int given' );
+ $this->driver->query( 'SELECT 1', PDO::FETCH_INTO, 1 );
+ }
+
+ public function test_exec(): void {
+ $result = $this->driver->exec( 'SELECT 1' );
+ $this->assertEquals( 0, $result );
+
+ $result = $this->driver->exec( 'CREATE TABLE t (id INT)' );
+ $this->assertEquals( 0, $result );
+
+ $result = $this->driver->exec( 'INSERT INTO t (id) VALUES (1)' );
+ $this->assertEquals( 1, $result );
+
+ $result = $this->driver->exec( 'INSERT INTO t (id) VALUES (2), (3)' );
+ $this->assertEquals( 2, $result );
+
+ $result = $this->driver->exec( 'UPDATE t SET id = 10 + id WHERE id = 0' );
+ $this->assertEquals( 0, $result );
+
+ $result = $this->driver->exec( 'UPDATE t SET id = 10 + id WHERE id = 1' );
+ $this->assertEquals( 1, $result );
+
+ $result = $this->driver->exec( 'UPDATE t SET id = 10 + id WHERE id < 10' );
+ $this->assertEquals( 2, $result );
+
+ $result = $this->driver->exec( 'DELETE FROM t WHERE id = 11' );
+ $this->assertEquals( 1, $result );
+
+ $result = $this->driver->exec( 'DELETE FROM t' );
+ $this->assertEquals( 2, $result );
+
+ $result = $this->driver->exec( 'DROP TABLE t' );
+ $this->assertEquals( 0, $result );
}
public function test_begin_transaction(): void {
@@ -50,4 +276,162 @@ public function test_rollback_no_active_transaction(): void {
$this->expectExceptionCode( 0 );
$this->driver->rollBack();
}
+
+ public function test_fetch_default(): void {
+ // Default fetch mode is PDO::FETCH_BOTH.
+ $result = $this->driver->query( "SELECT 1, 'abc', 2" );
+ if ( PHP_VERSION_ID < 80000 ) {
+ $this->assertSame(
+ array(
+ 1 => '1',
+ 2 => '2',
+ 'abc' => 'abc',
+ 3 => 'abc',
+ 4 => '2',
+ ),
+ $result->fetch()
+ );
+ } else {
+ $this->assertSame(
+ array(
+ 1 => '1',
+ 0 => '1',
+ 'abc' => 'abc',
+ '2' => '2',
+ ),
+ $result->fetch()
+ );
+ }
+ }
+
+ /**
+ * @dataProvider data_pdo_fetch_methods
+ */
+ public function test_fetch( $query, $mode, $expected ): void {
+ $stmt = $this->driver->query( $query );
+ $result = $stmt->fetch( $mode );
+
+ if ( is_object( $expected ) ) {
+ $this->assertInstanceOf( get_class( $expected ), $result );
+ $this->assertEquals( $expected, $result );
+ } elseif ( PDO::FETCH_NAMED === $mode ) {
+ // PDO::FETCH_NAMED returns all array keys as strings, even numeric
+ // ones. This is not possible in plain PHP and might be a PDO bug.
+ $this->assertSame( array_map( 'strval', array_keys( $expected ) ), array_keys( $result ) );
+ $this->assertSame( array_values( $expected ), array_values( $result ) );
+ } else {
+ $this->assertSame( $expected, $result );
+ }
+ }
+
+ public function test_attr_default_fetch_mode(): void {
+ $this->driver->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_NUM );
+ $result = $this->driver->query( "SELECT 'a', 'b', 'c'" );
+ $this->assertSame(
+ array( 'a', 'b', 'c' ),
+ $result->fetch()
+ );
+
+ $this->driver->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC );
+ $result = $this->driver->query( "SELECT 'a', 'b', 'c'" );
+ $this->assertSame(
+ array(
+ 'a' => 'a',
+ 'b' => 'b',
+ 'c' => 'c',
+ ),
+ $result->fetch()
+ );
+ }
+
+ public function test_attr_stringify_fetches(): void {
+ $this->driver->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
+ $result = $this->driver->query( "SELECT 123, 1.23, 'abc', true, false" );
+ $this->assertSame(
+ array( '123', '1.23', 'abc', '1', '0' ),
+ $result->fetch( PDO::FETCH_NUM )
+ );
+
+ $this->driver->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, false );
+ $result = $this->driver->query( "SELECT 123, 1.23, 'abc', true, false" );
+ $this->assertSame(
+ /*
+ * On PHP < 8.1, "PDO::ATTR_STRINGIFY_FETCHES" set to "false" has no
+ * effect when "PDO::ATTR_EMULATE_PREPARES" is "true" (the default).
+ *
+ * TODO: Consider supporting non-string values on PHP < 8.1 when both
+ * "PDO::ATTR_STRINGIFY_FETCHES" and "PDO::ATTR_EMULATE_PREPARES"
+ * are set to "false". This would require emulating the behavior,
+ * as PDO SQLite on PHP < 8.1 seems to always return strings.
+ */
+ PHP_VERSION_ID < 80100
+ ? array( '123', '1.23', 'abc', '1', '0' )
+ : array( 123, 1.23, 'abc', 1, 0 ),
+ $result->fetch( PDO::FETCH_NUM )
+ );
+ }
+
+ public function data_pdo_fetch_methods(): Generator {
+ // PDO::FETCH_BOTH
+ yield 'PDO::FETCH_BOTH' => array(
+ "SELECT 1, 'abc', 2, 'two' as `2`",
+ PDO::FETCH_BOTH,
+ PHP_VERSION_ID < 80000
+ ? array(
+ 1 => '1',
+ 2 => 'two',
+ 'abc' => 'abc',
+ 3 => 'abc',
+ 4 => '2',
+ 5 => 'two',
+ )
+ : array(
+ 1 => '1',
+ 0 => '1',
+ 'abc' => 'abc',
+ 2 => 'two',
+ 3 => 'two',
+ ),
+ );
+
+ // PDO::FETCH_NUM
+ yield 'PDO::FETCH_NUM' => array(
+ "SELECT 1, 'abc', 2, 'two' as `2`",
+ PDO::FETCH_NUM,
+ array( '1', 'abc', '2', 'two' ),
+ );
+
+ // PDO::FETCH_ASSOC
+ yield 'PDO::FETCH_ASSOC' => array(
+ "SELECT 1, 'abc', 2, 'two' as `2`",
+ PDO::FETCH_ASSOC,
+ array(
+ 1 => '1',
+ 'abc' => 'abc',
+ 2 => 'two',
+ ),
+ );
+
+ // PDO::FETCH_NAMED
+ yield 'PDO::FETCH_NAMED' => array(
+ "SELECT 1, 'abc', 2, 'two' as `2`",
+ PDO::FETCH_NAMED,
+ array(
+ 1 => '1',
+ 'abc' => 'abc',
+ 2 => array( '2', 'two' ),
+ ),
+ );
+
+ // PDO::FETCH_OBJ
+ yield 'PDO::FETCH_OBJ' => array(
+ "SELECT 1, 'abc', 2, 'two' as `2`",
+ PDO::FETCH_OBJ,
+ (object) array(
+ 1 => '1',
+ 'abc' => 'abc',
+ 2 => 'two',
+ ),
+ );
+ }
}
diff --git a/tests/WP_SQLite_Driver_Metadata_Tests.php b/tests/WP_SQLite_Driver_Metadata_Tests.php
index 9b201905..daff6ecd 100644
--- a/tests/WP_SQLite_Driver_Metadata_Tests.php
+++ b/tests/WP_SQLite_Driver_Metadata_Tests.php
@@ -687,10 +687,7 @@ public function testTruncateTable() {
'TRUNCATE TABLE wp_comments;'
);
$actual = $this->engine->get_query_results();
- $this->assertEquals(
- true,
- $actual
- );
+ $this->assertNull( $actual );
$this->assertTableEmpty( 'wp_comments', true );
}
diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php
index 794a9350..71cd0686 100644
--- a/tests/WP_SQLite_Driver_Tests.php
+++ b/tests/WP_SQLite_Driver_Tests.php
@@ -3067,6 +3067,81 @@ public function testCalcFoundRows() {
);
}
+ public function testFoundRowsWithoutSqlCalcFoundRows(): void {
+ $this->assertQuery( 'DROP TABLE _dates' );
+ $this->assertQuery( 'DROP TABLE _options' );
+
+ // CREATE TABLE
+ $this->assertQuery( 'CREATE TABLE t (id INT PRIMARY KEY, value TEXT, INDEX idx_value (value))' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertSame( '0', $result[0]->{'FOUND_ROWS()'} );
+
+ // INSERT
+ $this->assertQuery( 'INSERT INTO t (id) VALUES (1), (2), (3)' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertSame( '0', $result[0]->{'FOUND_ROWS()'} );
+
+ // SELECT
+ $this->assertQuery( 'SELECT * FROM t' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertSame( '3', $result[0]->{'FOUND_ROWS()'} );
+
+ // DESCRIBE
+ $this->assertQuery( 'DESCRIBE t' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertSame( '2', $result[0]->{'FOUND_ROWS()'} );
+
+ // SHOW COLLATION
+ $this->assertQuery( 'SHOW COLLATION' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertSame( '7', $result[0]->{'FOUND_ROWS()'} );
+
+ // SHOW DATABASES
+ $this->assertQuery( 'SHOW DATABASES' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertSame( '2', $result[0]->{'FOUND_ROWS()'} );
+
+ // SHOW COLUMNS
+ $this->assertQuery( 'SHOW COLUMNS FROM t' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertSame( '2', $result[0]->{'FOUND_ROWS()'} );
+
+ // SHOW CREATE TABLE
+ $this->assertQuery( 'SHOW CREATE TABLE t' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertSame( '1', $result[0]->{'FOUND_ROWS()'} );
+
+ // SHOW CREATE TABLE with non-existent table
+ $this->assertQuery( 'SHOW CREATE TABLE non_existent_table' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertSame( '0', $result[0]->{'FOUND_ROWS()'} );
+
+ // SHOW INDEX
+ $this->assertQuery( 'SHOW INDEX FROM t' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertSame( '2', $result[0]->{'FOUND_ROWS()'} );
+
+ // SHOW GRANTS
+ $this->assertQuery( 'SHOW GRANTS' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertSame( '1', $result[0]->{'FOUND_ROWS()'} );
+
+ // SHOW TABLE STATUS
+ $r = $this->assertQuery( 'SHOW TABLE STATUS' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertSame( '1', $result[0]->{'FOUND_ROWS()'} );
+
+ // SHOW TABLES
+ $this->assertQuery( 'SHOW TABLES' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertSame( '1', $result[0]->{'FOUND_ROWS()'} );
+
+ // SHOW VARIABLES
+ $this->assertQuery( 'SHOW VARIABLES' );
+ $result = $this->assertQuery( 'SELECT FOUND_ROWS()' );
+ $this->assertSame( '0', $result[0]->{'FOUND_ROWS()'} );
+ }
+
public function testComplexSelectBasedOnDates() {
$this->assertQuery(
"INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');"
diff --git a/wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php b/wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php
index 659b3f1c..80cb1f67 100644
--- a/wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php
+++ b/wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php
@@ -14,7 +14,7 @@
*
* The driver requires PDO with the SQLite driver, and the PCRE engine.
*/
-class WP_PDO_MySQL_On_SQLite {
+class WP_PDO_MySQL_On_SQLite extends PDO {
/**
* The path to the MySQL SQL grammar file.
*/
@@ -458,18 +458,22 @@ class WP_PDO_MySQL_On_SQLite {
private $last_sqlite_queries = array();
/**
- * Results of the last emulated query.
+ * A PDO SQLite statement that represents the result of the last emulated query.
*
- * @var array|null
+ * @var PDOStatement|null
*/
- private $last_result;
+ private $last_result_statement;
/**
- * Return value of the last emulated query.
+ * Override for the number of affected rows by the last emulated query.
*
- * @var mixed
+ * By default, the number of affected rows is carried by the row count value
+ * of "$this->last_result_statement". This property serves as an override for
+ * when the row count of the emulated query and statement don't match.
+ *
+ * @var int|null
*/
- private $last_return_value;
+ private $last_affected_rows;
/**
* SQLite column metadata for the last emulated query.
@@ -479,11 +483,26 @@ class WP_PDO_MySQL_On_SQLite {
private $last_column_meta = array();
/**
- * Number of rows found by the last SQL_CALC_FOUND_ROW query.
+ * Data for emulating the "FOUND_ROWS()" function.
*
- * @var int
+ * When "SQL_CALC_FOUND_ROWS" is used, the appropriate value is stored here.
+ * Otherwise, it's used to store the last number of found rows, or a query
+ * that returns the rows that need to be counted for usage in "FOUND_ROWS()".
+ *
+ * From MySQL documentation:
+ * In the absence of the SQL_CALC_FOUND_ROWS option in the most recent
+ * successful SELECT statement, FOUND_ROWS() returns the number of rows
+ * in the result set returned by that statement.
+ *
+ * In reality, this applies to SHOW and DESCRIBE statements as well.
+ *
+ * The value can be:
+ * - integer: The number of rows to be directly returned by "FOUND_ROWS()".
+ * - string: A SQLite query whose result set rows need to be counted.
+ *
+ * @var int|string
*/
- private $last_sql_calc_found_rows = null;
+ private $found_rows = 0;
/**
* Whether the current MySQL query is read-only.
@@ -579,24 +598,70 @@ class WP_PDO_MySQL_On_SQLite {
private $user_variables = array();
/**
- * Constructor.
+ * PDO API: Constructor.
*
* Set up an SQLite connection and the MySQL-on-SQLite driver.
*
* @param WP_SQLite_Connection $connection A SQLite database connection.
- * @param string $database The database name.
+ * @param string $db_name The database name.
*
* @throws WP_SQLite_Driver_Exception When the driver initialization fails.
*/
public function __construct(
- WP_SQLite_Connection $connection,
- string $database,
- int $mysql_version = 80038
+ string $dsn,
+ ?string $username = null,
+ ?string $password = null,
+ array $options = array()
) {
- $this->mysql_version = $mysql_version;
- $this->connection = $connection;
- $this->main_db_name = $database;
- $this->db_name = $database;
+ // PDO DSN can't include "\0" bytes; parsing stops at the first one.
+ $first_null_byte_index = strpos( $dsn, "\0" );
+ if ( false !== $first_null_byte_index ) {
+ $dsn = substr( $dsn, 0, $first_null_byte_index );
+ }
+
+ // Parse the DSN.
+ $dsn_parts = explode( ':', $dsn, 2 );
+ if ( count( $dsn_parts ) < 2 ) {
+ throw new PDOException( 'invalid data source name' );
+ }
+
+ $driver = $dsn_parts[0];
+ if ( 'mysql-on-sqlite' !== $driver ) {
+ throw new PDOException( 'could not find driver' );
+ }
+
+ // PDO DSN supports semicolon escaping using double semicolon sequences.
+ // Replace ";;" with "\0" to preserve escaped semicolons in "explode()".
+ $args_string = str_replace( ';;', "\0", $dsn_parts[1] );
+ $args = array();
+ foreach ( explode( ';', $args_string ) as $arg ) {
+ // Restore escaped semicolons that were replaced with "\0".
+ $arg = str_replace( "\0", ';', $arg );
+
+ // PDO DSN allows whitespace before argument name. Trim characters
+ // as per the "isspace()" C function (in the default "C" locale).
+ $arg = ltrim( $arg, " \n\r\t\v\f" );
+
+ if ( '' === $arg ) {
+ continue;
+ }
+ $arg_parts = explode( '=', $arg, 2 );
+ $args[ $arg_parts[0] ] = $arg_parts[1] ?? null;
+ }
+
+ $path = $args['path'] ?? ':memory:';
+ $db_name = $args['dbname'] ?? 'sqlite_database';
+
+ // Create a new SQLite connection.
+ if ( isset( $options['pdo'] ) ) {
+ $this->connection = new WP_SQLite_Connection( array( 'pdo' => $options['pdo'] ) );
+ } else {
+ $this->connection = new WP_SQLite_Connection( array( 'path' => $path ) );
+ }
+
+ $this->mysql_version = $options['mysql_version'] ?? 80038;
+ $this->main_db_name = $db_name;
+ $this->db_name = $db_name;
// Check the database name.
if ( '' === $this->db_name ) {
@@ -685,7 +750,7 @@ function ( string $sql, array $params ) {
}
/**
- * Translate and execute a MySQL query in SQLite.
+ * PDO API: Translate and execute a MySQL query in SQLite.
*
* A single MySQL query can be translated into zero or more SQLite queries.
*
@@ -696,15 +761,98 @@ function ( string $sql, array $params ) {
* @return mixed Return value, depending on the query type.
*
* @throws WP_SQLite_Driver_Exception When the query execution fails.
- *
- * TODO:
- * The API of this function is not final.
- * We should also add support for parametrized queries.
- * See: https://github.com/Automattic/sqlite-database-integration/issues/7
*/
- public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) {
+ #[ReturnTypeWillChange]
+ public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_args ) {
+ // Validate and parse the fetch mode and arguments.
+ $arg_count = func_num_args();
+ $arg_colno = 0;
+ $arg_class = null;
+ $arg_constructor_args = array();
+ $arg_into = null;
+
+ $get_type = function ( $value ) {
+ $type = gettype( $value );
+ if ( 'boolean' === $type ) {
+ return 'bool';
+ } elseif ( 'integer' === $type ) {
+ return 'int';
+ } elseif ( 'double' === $type ) {
+ return 'float';
+ }
+ return $type;
+ };
+
+ if ( null === $fetch_mode ) {
+ if ( PHP_VERSION_ID < 80100 && func_num_args() > 1 ) {
+ trigger_error(
+ 'PDO::query(): SQLSTATE[HY000]: General error: mode must be an integer',
+ E_USER_WARNING
+ );
+ return false;
+ }
+
+ // When the default FETCH_BOTH is not set explicitly, additional
+ // arguments are ignored, and the argument count is not validated.
+ $fetch_mode = $this->connection->get_pdo()->getAttribute( PDO::ATTR_DEFAULT_FETCH_MODE );
+ $fetch_mode_args = array();
+ } elseif ( PDO::FETCH_COLUMN === $fetch_mode ) {
+ if ( 3 !== $arg_count ) {
+ throw new ArgumentCountError(
+ sprintf( 'PDO::query() expects exactly 3 arguments for the fetch mode provided, %d given', $arg_count )
+ );
+ }
+ if ( ! is_int( $fetch_mode_args[0] ) ) {
+ throw new TypeError(
+ sprintf( 'PDO::query(): Argument #3 must be of type int, %s given', $get_type( $fetch_mode_args[0] ) )
+ );
+ }
+ $arg_colno = $fetch_mode_args[0];
+ } elseif ( PDO::FETCH_CLASS === $fetch_mode ) {
+ if ( $arg_count < 3 ) {
+ throw new ArgumentCountError(
+ sprintf( 'PDO::query() expects at least 3 arguments for the fetch mode provided, %d given', $arg_count )
+ );
+ }
+ if ( $arg_count > 4 ) {
+ throw new ArgumentCountError(
+ sprintf( 'PDO::query() expects at most 4 arguments for the fetch mode provided, %d given', $arg_count )
+ );
+ }
+ if ( ! is_string( $fetch_mode_args[0] ) ) {
+ throw new TypeError(
+ sprintf( 'PDO::query(): Argument #3 must be of type string, %s given', $get_type( $fetch_mode_args[0] ) )
+ );
+ }
+ if ( ! class_exists( $fetch_mode_args[0] ) ) {
+ throw new TypeError( 'PDO::query(): Argument #3 must be a valid class' );
+ }
+ if ( 4 === $arg_count && ! is_array( $fetch_mode_args[1] ) ) {
+ throw new TypeError(
+ sprintf( 'PDO::query(): Argument #4 must be of type ?array, %s given', $get_type( $fetch_mode_args[1] ) )
+ );
+ }
+ $arg_class = $fetch_mode_args[0];
+ $arg_constructor_args = $fetch_mode_args[1] ?? array();
+ } elseif ( PDO::FETCH_INTO === $fetch_mode ) {
+ if ( 3 !== $arg_count ) {
+ throw new ArgumentCountError(
+ sprintf( 'PDO::query() expects exactly 3 arguments for the fetch mode provided, %d given', $arg_count )
+ );
+ }
+ if ( ! is_object( $fetch_mode_args[0] ) ) {
+ throw new TypeError(
+ sprintf( 'PDO::query(): Argument #3 must be of type object, %s given', $get_type( $fetch_mode_args[0] ) )
+ );
+ }
+ $arg_into = $fetch_mode_args[0];
+ } elseif ( $arg_count > 2 ) {
+ throw new ArgumentCountError(
+ sprintf( 'PDO::query() expects exactly 2 arguments for the fetch mode provided, %d given', $arg_count )
+ );
+ }
+
$this->flush();
- $this->pdo_fetch_mode = $fetch_mode;
$this->last_mysql_query = $query;
try {
@@ -748,7 +896,14 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
if ( $wrap_in_transaction ) {
$this->commit_wrapper_transaction();
}
- return $this->last_return_value;
+
+ if ( null === $this->last_result_statement ) {
+ $this->last_result_statement = $this->create_result_statement_from_data( array(), array() );
+ }
+
+ $stmt = new WP_PDO_Proxy_Statement( $this->last_result_statement, $this->last_affected_rows );
+ $stmt->setFetchMode( $fetch_mode, ...$fetch_mode_args );
+ return $stmt;
} catch ( Throwable $e ) {
try {
$this->rollback_user_transaction();
@@ -762,9 +917,25 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo
throw $this->convert_information_schema_exception( $e );
}
throw $this->new_driver_exception( $e->getMessage(), $e->getCode(), $e );
+ } finally {
+ // A query that doesn't return any rows or fails sets found rows to 0.
+ if ( ! $this->is_readonly || isset( $e ) ) {
+ $this->found_rows = 0;
+ }
}
}
+ /**
+ * PDO API: Execute a MySQL statement and return the number of affected rows.
+ *
+ * @return int|false The number of affected rows or false on failure.
+ */
+ #[ReturnTypeWillChange]
+ public function exec( $query ) {
+ $stmt = $this->query( $query );
+ return $stmt->rowCount();
+ }
+
/**
* PDO API: Begin a transaction.
*
@@ -834,6 +1005,36 @@ public function inTransaction(): bool {
return $this->connection->get_pdo()->inTransaction();
}
+ /**
+ * PDO API: Set a PDO attribute.
+ *
+ * TODO: Evaluate whether we should pass all PDO attributes to the PDO SQLite
+ * instance, or whether some of them require special handling.
+ * See: https://github.com/php/php-src/blob/b391c28f903536e3bc6a0021ae0976ddbc2745f8/ext/pdo/php_pdo_driver.h#L103
+ *
+ * @param int $attribute The attribute to set.
+ * @param mixed $value The value of the attribute.
+ * @return bool True on success, false on failure.
+ */
+ public function setAttribute( $attribute, $value ): bool {
+ return $this->connection->get_pdo()->setAttribute( $attribute, $value );
+ }
+
+ /**
+ * PDO API: Get a PDO attribute.
+ *
+ * TODO: Evaluate whether we should get all PDO attributes from the PDO SQLite
+ * instance, or whether some of them require special handling.
+ * See: https://github.com/php/php-src/blob/b391c28f903536e3bc6a0021ae0976ddbc2745f8/ext/pdo/php_pdo_driver.h#L103
+ *
+ * @param int $attribute The attribute to get.
+ * @return mixed The value of the attribute.
+ */
+ #[ReturnTypeWillChange]
+ public function getAttribute( $attribute ) {
+ return $this->connection->get_pdo()->getAttribute( $attribute );
+ }
+
/**
* Get the SQLite connection instance.
*
@@ -937,24 +1138,6 @@ public function create_parser( string $query ): WP_MySQL_Parser {
return new WP_MySQL_Parser( self::$mysql_grammar, $tokens );
}
- /**
- * Get results of the last query.
- *
- * @return mixed
- */
- public function get_query_results() {
- return $this->last_result;
- }
-
- /**
- * Get return value of the last query() function call.
- *
- * @return mixed
- */
- public function get_last_return_value() {
- return $this->last_return_value;
- }
-
/**
* Get the number of columns returned by the last emulated query.
*
@@ -1248,9 +1431,8 @@ private function execute_mysql_query( WP_Parser_Node $node ): void {
$this->execute_drop_index_statement( $node );
break;
default:
- $query = $this->translate( $node );
- $this->execute_sqlite_query( $query );
- $this->set_result_from_affected_rows();
+ $query = $this->translate( $node );
+ $this->last_result_statement = $this->execute_sqlite_query( $query );
}
break;
case 'truncateTableStatement':
@@ -1623,9 +1805,9 @@ private function execute_select_statement( WP_Parser_Node $node ): void {
'SELECT COUNT(*) AS cnt FROM (' . $this->translate( $count_expr ) . ')'
);
- $this->last_sql_calc_found_rows = $result->fetchColumn();
+ $this->found_rows = (int) $result->fetchColumn();
} else {
- $this->last_sql_calc_found_rows = null;
+ $this->found_rows = $query;
}
// Execute the query.
@@ -1634,9 +1816,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->store_last_column_meta_from_statement( $stmt );
- $this->set_results_from_fetched_data(
- $stmt->fetchAll( $this->pdo_fetch_mode )
- );
+ $this->last_result_statement = $stmt;
}
/**
@@ -1721,8 +1901,7 @@ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): vo
*/
if ( null !== $on_conflict_update_list ) {
try {
- $this->execute_sqlite_query( $query );
- $this->set_result_from_affected_rows();
+ $this->last_result_statement = $this->execute_sqlite_query( $query );
} catch ( PDOException $e ) {
$unique_key_violation_prefix = 'SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: ';
if ( '23000' === $e->getCode() && str_contains( $e->getMessage(), $unique_key_violation_prefix ) ) {
@@ -1737,22 +1916,21 @@ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): vo
* prefix and the "
." part for the first column, and
* then split the rest of the list by ", ." sequence.
*/
- $column_list = substr( $e->getMessage(), strlen( $unique_key_violation_prefix ) + strlen( $table_name ) + 1 );
- $column_names = explode( ", $table_name.", $column_list );
- $quoted_column_names = array_map(
+ $column_list = substr( $e->getMessage(), strlen( $unique_key_violation_prefix ) + strlen( $table_name ) + 1 );
+ $column_names = explode( ", $table_name.", $column_list );
+ $quoted_column_names = array_map(
function ( $column ) {
return $this->quote_sqlite_identifier( $column );
},
$column_names
);
- $this->execute_sqlite_query(
+ $this->last_result_statement = $this->execute_sqlite_query(
$query . sprintf(
' ON CONFLICT(%s) DO UPDATE SET %s',
implode( ', ', $quoted_column_names ),
$on_conflict_update_list
)
);
- $this->set_result_from_affected_rows();
} else {
throw $e;
}
@@ -1760,8 +1938,7 @@ function ( $column ) {
return;
}
- $this->execute_sqlite_query( $query );
- $this->set_result_from_affected_rows();
+ $this->last_result_statement = $this->execute_sqlite_query( $query );
}
/**
@@ -1997,8 +2174,7 @@ private function execute_update_statement( WP_Parser_Node $node ): void {
);
$query = implode( ' ', array_filter( $parts ) );
- $this->execute_sqlite_query( $query );
- $this->set_result_from_affected_rows();
+ $this->last_result_statement = $this->execute_sqlite_query( $query );
}
/**
@@ -2078,10 +2254,10 @@ private function execute_delete_statement( WP_Parser_Node $node ): void {
)->fetchAll( PDO::FETCH_ASSOC );
// 4. Execute DELETE statements for each table.
- $rows = 0;
+ $affected_rows = 0;
if ( count( $ids ) > 0 ) {
foreach ( $table_aliases as $table ) {
- $this->execute_sqlite_query(
+ $stmt = $this->execute_sqlite_query(
sprintf(
'DELETE FROM %s AS %s WHERE rowid IN ( %s )',
$this->quote_sqlite_identifier( $alias_map[ $table ] ),
@@ -2089,12 +2265,12 @@ private function execute_delete_statement( WP_Parser_Node $node ): void {
implode( ', ', array_column( $ids, "{$table}_rowid" ) )
)
);
- $this->set_result_from_affected_rows();
- $rows += $this->last_result;
+ $affected_rows += $stmt->rowCount();
}
}
- $this->set_result_from_affected_rows( $rows );
+ $this->last_result_statement = $this->create_result_statement_from_data( array(), array() );
+ $this->last_affected_rows = $affected_rows;
return;
}
@@ -2106,9 +2282,8 @@ private function execute_delete_statement( WP_Parser_Node $node ): void {
throw $this->new_access_denied_to_information_schema_exception();
}
- $query = $this->translate( $node );
- $this->execute_sqlite_query( $query );
- $this->set_result_from_affected_rows();
+ $query = $this->translate( $node );
+ $this->last_result_statement = $this->execute_sqlite_query( $query );
}
/**
@@ -2159,7 +2334,7 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void {
)->fetchColumn();
if ( $table_exists ) {
- $this->set_result_from_affected_rows( 0 );
+ $this->last_result_statement = $this->create_result_statement_from_data( array(), array() );
return;
}
}
@@ -2327,7 +2502,10 @@ private function execute_truncate_table_statement( WP_Parser_Node $node ): void
sprintf( 'DELETE FROM %s', $this->quote_sqlite_identifier( $table_name ) )
);
try {
- $this->execute_sqlite_query( 'DELETE FROM sqlite_sequence WHERE name = ?', array( $table_name ) );
+ $this->last_result_statement = $this->execute_sqlite_query(
+ 'DELETE FROM sqlite_sequence WHERE name = ?',
+ array( $table_name )
+ );
} catch ( PDOException $e ) {
if ( str_contains( $e->getMessage(), 'no such table' ) ) {
// The table might not exist if no sequences are used in the DB.
@@ -2335,7 +2513,6 @@ private function execute_truncate_table_statement( WP_Parser_Node $node ): void
throw $e;
}
}
- $this->set_result_from_affected_rows();
}
/**
@@ -2477,18 +2654,6 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
$table_is_temporary = $this->information_schema_builder->temporary_table_exists( $table_name );
$sql = $this->get_mysql_create_table_statement( $table_is_temporary, $table_name );
- if ( null === $sql ) {
- $this->set_results_from_fetched_data( array() );
- } else {
- $this->set_results_from_fetched_data(
- array(
- (object) array(
- 'Table' => $table_name,
- 'Create Table' => $sql,
- ),
- )
- );
- }
$this->last_column_meta = array(
array(
@@ -2510,6 +2675,12 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
'precision' => 31,
),
);
+
+ $this->last_result_statement = $this->create_result_statement_from_data(
+ array_column( $this->last_column_meta, 'name' ),
+ null === $sql ? array() : array( array( $table_name, $sql ) )
+ );
+ $this->found_rows = null === $sql ? 0 : 1;
return;
}
break;
@@ -2519,14 +2690,11 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
$this->execute_show_index_statement( $node );
return;
case WP_MySQL_Lexer::GRANTS_SYMBOL:
- $this->set_results_from_fetched_data(
- array(
- (object) array(
- '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_result_statement = $this->create_result_statement_from_data(
+ array( 'Grants for root@%' ),
+ array( array( '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(
+ $this->last_column_meta = array(
array(
'native_type' => 'STRING',
'pdo_type' => PDO::PARAM_STR,
@@ -2537,6 +2705,7 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
'precision' => 31,
),
);
+ $this->found_rows = 1;
return;
case WP_MySQL_Lexer::TABLE_SYMBOL:
$this->execute_show_table_status_statement( $node );
@@ -2545,8 +2714,7 @@ 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_column_meta = array(
+ $this->last_column_meta = array(
array(
'native_type' => 'STRING',
'pdo_type' => PDO::PARAM_STR,
@@ -2566,6 +2734,11 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
'precision' => 0,
),
);
+ $this->last_result_statement = $this->create_result_statement_from_data(
+ array_column( $this->last_column_meta, 'name' ),
+ array()
+ );
+ $this->found_rows = 0;
return;
}
@@ -2593,24 +2766,24 @@ private function execute_show_collation_statement( WP_Parser_Node $node ): void
$condition = $this->translate_show_like_or_where_condition( $like_or_where, 'collation_name' );
}
- $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)
- WHERE TRUE %s',
- $definition,
- $condition ?? ''
- )
+ $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)
+ WHERE TRUE %s',
+ $definition,
+ $condition ?? ''
);
+ $stmt = $this->execute_sqlite_query( $query );
$this->store_last_column_meta_from_statement( $stmt );
- $this->set_results_from_fetched_data( $stmt->fetchAll( PDO::FETCH_OBJ ) );
+ $this->last_result_statement = $stmt;
+ $this->found_rows = $query;
}
/**
@@ -2626,26 +2799,23 @@ private function execute_show_databases_statement( WP_Parser_Node $node ): void
if ( $like_or_where ) {
$condition = $this->translate_show_like_or_where_condition( $like_or_where, 'schema_name' );
}
- $stmt = $this->execute_sqlite_query(
- sprintf(
- 'SELECT SCHEMA_NAME AS Database
- FROM (
- SELECT CASE WHEN SCHEMA_NAME = ? THEN ? ELSE SCHEMA_NAME END AS SCHEMA_NAME
- FROM %s
- ORDER BY SCHEMA_NAME
- )%s',
- $this->quote_sqlite_identifier( $schemata_table ),
- isset( $condition ) ? ( ' WHERE TRUE ' . $condition ) : ''
- ),
- array(
- $this->get_saved_db_name(),
- $this->main_db_name,
- )
+ $query = sprintf(
+ 'SELECT SCHEMA_NAME AS Database
+ FROM (
+ SELECT CASE WHEN SCHEMA_NAME = %s THEN %s ELSE SCHEMA_NAME END AS SCHEMA_NAME
+ FROM %s
+ ORDER BY SCHEMA_NAME
+ )%s',
+ $this->connection->quote( $this->get_saved_db_name() ),
+ $this->connection->quote( $this->main_db_name ),
+ $this->quote_sqlite_identifier( $schemata_table ),
+ isset( $condition ) ? ( ' WHERE TRUE ' . $condition ) : ''
);
+ $stmt = $this->execute_sqlite_query( $query );
$this->store_last_column_meta_from_statement( $stmt );
- $databases = $stmt->fetchAll( PDO::FETCH_OBJ );
- $this->set_results_from_fetched_data( $databases );
+ $this->last_result_statement = $stmt;
+ $this->found_rows = $query;
}
/**
@@ -2695,8 +2865,8 @@ private function execute_show_index_statement( WP_Parser_Node $node ): void {
*/
$statistics_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'statistics' );
- $stmt = $this->execute_sqlite_query(
- '
+ $query = sprintf(
+ "
SELECT
TABLE_NAME AS `Table`,
NON_UNIQUE AS `Non_unique`,
@@ -2713,10 +2883,10 @@ private function execute_show_index_statement( WP_Parser_Node $node ): void {
INDEX_COMMENT AS `Index_comment`,
IS_VISIBLE AS `Visible`,
EXPRESSION AS `Expression`
- FROM ' . $this->quote_sqlite_identifier( $statistics_table ) . "
- WHERE table_schema = ?
- AND table_name = ?
- $condition
+ FROM %s
+ WHERE table_schema = %s
+ AND table_name = %s
+ %s
ORDER BY
INDEX_NAME = 'PRIMARY' DESC,
NON_UNIQUE = '0' DESC,
@@ -2726,12 +2896,16 @@ private function execute_show_index_statement( WP_Parser_Node $node ): void {
ROWID,
SEQ_IN_INDEX
",
- array( $this->get_saved_db_name( $database ), $table_name )
+ $this->quote_sqlite_identifier( $statistics_table ),
+ $this->connection->quote( $this->get_saved_db_name( $database ) ),
+ $this->connection->quote( $table_name ),
+ $condition
);
+ $stmt = $this->execute_sqlite_query( $query );
$this->store_last_column_meta_from_statement( $stmt );
- $index_info = $stmt->fetchAll( PDO::FETCH_OBJ );
- $this->set_results_from_fetched_data( $index_info );
+ $this->last_result_statement = $stmt;
+ $this->found_rows = $query;
}
/**
@@ -2762,42 +2936,38 @@ private function execute_show_table_status_statement( WP_Parser_Node $node ): vo
false, // SHOW TABLE STATUS lists only non-temporary tables.
'tables'
);
- $stmt = $this->execute_sqlite_query(
- sprintf(
- '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 ) )
+ $query = sprintf(
+ '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 %s
+ ORDER BY table_name',
+ $this->quote_sqlite_identifier( $tables_tables ),
+ $this->connection->quote( $this->get_saved_db_name( $database ) ),
+ $condition ?? ''
);
+ $stmt = $this->execute_sqlite_query( $query );
$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() );
- }
- $this->set_results_from_fetched_data( $table_info );
+ $this->last_result_statement = $stmt;
+ $this->found_rows = $query;
}
/**
@@ -2832,24 +3002,20 @@ private function execute_show_tables_statement( WP_Parser_Node $node ): void {
false, // SHOW TABLES lists only non-temporary tables.
'tables'
);
- $stmt = $this->execute_sqlite_query(
- sprintf(
- '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 ) )
+ $query = sprintf(
+ 'SELECT %s FROM %s WHERE table_schema = %s %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 ),
+ $this->connection->quote( $this->get_saved_db_name( $database ) ),
+ $condition ?? ''
);
+ $stmt = $this->execute_sqlite_query( $query );
$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() );
- }
- $this->set_results_from_fetched_data( $table_info );
+ $this->last_result_statement = $stmt;
+ $this->found_rows = $query;
}
/**
@@ -2899,30 +3065,27 @@ 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' );
- $stmt = $this->execute_sqlite_query(
- sprintf(
- '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 )
+ $query = sprintf(
+ '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 = %s AND table_name = %s %s
+ ORDER BY ordinal_position',
+ $this->quote_sqlite_identifier( $columns_table ),
+ $this->connection->quote( $this->get_saved_db_name( $database ) ),
+ $this->connection->quote( $table_name ),
+ $condition ?? ''
);
+ $stmt = $this->execute_sqlite_query( $query );
$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() );
- }
- $this->set_results_from_fetched_data( $column_info );
+ $this->last_result_statement = $stmt;
+ $this->found_rows = $query;
}
/**
@@ -2939,26 +3102,27 @@ 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' );
- $stmt = $this->execute_sqlite_query(
- '
- 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 ' . $this->quote_sqlite_identifier( $columns_table ) . '
- WHERE table_schema = ?
- AND table_name = ?
- ORDER BY ordinal_position
- ',
- array( $this->get_saved_db_name( $database ), $table_name )
+ $query = sprintf(
+ '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 = %s
+ AND table_name = %s
+ ORDER BY ordinal_position',
+ $this->quote_sqlite_identifier( $columns_table ),
+ $this->connection->quote( $this->get_saved_db_name( $database ) ),
+ $this->connection->quote( $table_name )
);
+ $stmt = $this->execute_sqlite_query( $query );
$this->store_last_column_meta_from_statement( $stmt );
- $column_info = $stmt->fetchAll( PDO::FETCH_OBJ );
- $this->set_results_from_fetched_data( $column_info );
+ $this->last_result_statement = $stmt;
+ $this->found_rows = $query;
}
/**
@@ -3093,7 +3257,7 @@ private function execute_set_statement( WP_Parser_Node $node ): void {
}
}
- $this->last_result = 0;
+ $this->last_result_statement = $this->create_result_statement_from_data( array(), array() );
}
/**
@@ -3279,14 +3443,14 @@ private function execute_administration_statement( WP_Parser_Node $node ): void
$operation = strtolower( $first_token->get_value() );
foreach ( $errors as $error ) {
- $results[] = (object) array(
+ $results[] = array(
'Table' => $this->db_name . '.' . $table_name,
'Op' => $operation,
'Msg_type' => 'Error',
'Msg_text' => $error,
);
}
- $results[] = (object) array(
+ $results[] = array(
'Table' => $this->db_name . '.' . $table_name,
'Op' => $operation,
'Msg_type' => 'status',
@@ -3294,7 +3458,7 @@ private function execute_administration_statement( WP_Parser_Node $node ): void
);
}
- $this->last_column_meta = array(
+ $this->last_column_meta = array(
array(
'native_type' => 'STRING',
'pdo_type' => PDO::PARAM_STR,
@@ -3332,7 +3496,10 @@ private function execute_administration_statement( WP_Parser_Node $node ): void
'precision' => 31,
),
);
- $this->set_results_from_fetched_data( $results );
+ $this->last_result_statement = $this->create_result_statement_from_data(
+ array_column( $this->last_column_meta, 'name' ),
+ $results
+ );
}
/**
@@ -4209,11 +4376,16 @@ private function translate_function_call( WP_Parser_Node $node ): string {
case 'CONCAT':
return '(' . implode( ' || ', $args ) . ')';
case 'FOUND_ROWS':
- $found_rows = $this->last_sql_calc_found_rows;
- if ( null === $found_rows && is_array( $this->last_result ) ) {
- $found_rows = count( $this->last_result );
+ $found_rows = $this->found_rows;
+ if ( is_int( $found_rows ) ) {
+ return $found_rows;
+ } elseif ( is_string( $found_rows ) ) {
+ return (int) $this->execute_sqlite_query(
+ sprintf( 'SELECT COUNT(*) FROM (%s)', $found_rows )
+ )->fetchColumn()[0];
+ } else {
+ return 0;
}
- return $found_rows;
case 'VERSION':
$version = (string) $this->mysql_version;
$value = sprintf(
@@ -6289,41 +6461,90 @@ private function quote_mysql_utf8_string_literal( string $utf8_literal ): string
private function flush(): void {
$this->last_mysql_query = '';
$this->last_sqlite_queries = array();
- $this->last_result = null;
- $this->last_return_value = null;
+ $this->last_result_statement = null;
+ $this->last_affected_rows = null;
$this->last_column_meta = array();
$this->is_readonly = false;
$this->wrapper_transaction_type = null;
}
/**
- * Set results of a query() call using fetched data.
+ * Create a PDO SQLite statement from the specified columns and rows.
*
- * @param array $data The data to set.
- */
- private function set_results_from_fetched_data( array $data ): void {
- $this->last_result = $data;
- $this->last_return_value = $this->last_result;
- }
-
- /**
- * Set results of a query() call using the number of affected rows.
+ * Some emulated MySQL queries don't have an SQLite counterpart and their
+ * result data may be generated without a corresponding SQLite statement.
+ * In such cases, we can generate a simple SQLite SELECT query that will
+ * provide us with the PDOStatement API for the given column and row data.
*
- * @param int|null $override Override the affected rows.
+ * @param array $columns The columns of the result set.
+ * @param array $rows The rows of the result set.
+ * @return PDOStatement The corresponding PDO SQLite statement.
*/
- private function set_result_from_affected_rows( ?int $override = null ): void {
+ private function create_result_statement_from_data( array $columns, array $rows ): PDOStatement {
+ $pdo = $this->connection->get_pdo();
+
/*
- * SELECT CHANGES() is a workaround for the fact that $stmt->rowCount()
- * returns "0" (zero) with the SQLite driver at all times.
- * See: https://www.php.net/manual/en/pdostatement.rowcount.php
+ * With 0 columns, we need to create a PDO statement that has no columns.
+ * This can be done using a noop INSERT statement that modifies no data.
*/
- if ( null === $override ) {
- $affected_rows = (int) $this->execute_sqlite_query( 'SELECT CHANGES()' )->fetch()[0];
- } else {
- $affected_rows = $override;
+ if ( 0 === count( $columns ) ) {
+ return $pdo->query(
+ sprintf(
+ 'INSERT INTO %s (rowid) SELECT NULL WHERE FALSE',
+ $this->quote_sqlite_identifier( self::GLOBAL_VARIABLES_TABLE_NAME )
+ )
+ );
+ }
+
+ /*
+ * Create an SQLite statement that returns the specified columns and rows.
+ * This can be done using a SELECT statement in the following form:
+ *
+ * -- A dummy header row to assign correct column names.
+ * SELECT NULL AS `col1`, NULL AS `col2`, ... WHERE FALSE
+ *
+ * UNION ALL
+ *
+ * -- The actual data rows.
+ * VALUES
+ * (val11, val12, ...),
+ * (val21, val22, ...),
+ * ...
+ */
+
+ // Construct column header row ("SELECT WHERE FALSE").
+ $query = 'SELECT ';
+ foreach ( $columns as $i => $column ) {
+ $query .= $i > 0 ? ', ' : '';
+ $query .= 'NULL AS ' . $pdo->quote( $column );
+ }
+ $query .= ' WHERE FALSE';
+
+ // UNION ALL
+ if ( count( $rows ) > 0 ) {
+ $query .= ' UNION ALL VALUES ';
+ }
+
+ // Construct data rows ("VALUES ").
+ foreach ( $rows as $i => $row ) {
+ $query .= $i > 0 ? ', ' : '';
+ $query .= '(';
+ foreach ( array_values( $row ) as $j => $value ) {
+ $query .= $j > 0 ? ', ' : '';
+ if ( null === $value ) {
+ $query .= 'NULL';
+ } elseif ( is_string( $value ) && strpos( $value, "\0" ) !== false ) {
+ // Handle null characters; see self::translate_string_literal().
+ $query .= sprintf( "CAST(x'%s' AS TEXT)", bin2hex( $value ) );
+ } elseif ( is_string( $value ) ) {
+ $query .= $pdo->quote( $value );
+ } else {
+ $query .= $value;
+ }
+ }
+ $query .= ')';
}
- $this->last_result = $affected_rows;
- $this->last_return_value = $affected_rows;
+ return $pdo->query( $query );
}
/**
diff --git a/wp-includes/sqlite-ast/class-wp-pdo-proxy-statement.php b/wp-includes/sqlite-ast/class-wp-pdo-proxy-statement.php
new file mode 100644
index 00000000..c5efd2b4
--- /dev/null
+++ b/wp-includes/sqlite-ast/class-wp-pdo-proxy-statement.php
@@ -0,0 +1,382 @@
+setDefaultFetchMode( $mode );
+ }
+ return $this->setDefaultFetchMode( $mode, $params );
+ }
+
+ /**
+ * Fetch all remaining rows from the result set.
+ *
+ * @param int $mode The fetch mode to use.
+ * @param mixed $class_name With PDO::FETCH_CLASS, the name of the class to instantiate.
+ * @param mixed $constructor_args With PDO::FETCH_CLASS, the parameters to pass to the class constructor.
+ * @return array The result set as an array of rows.
+ */
+ public function fetchAll( $mode = null, $class_name = null, $constructor_args = null ): array {
+ // Do not pass additional arguments when they are NULL to prevent
+ // "Extraneous additional parameters" error.
+ if ( null === $class_name && null === $constructor_args ) {
+ return $this->fetchAllRows( $mode );
+ }
+ return $this->fetchAllRows( $mode, $class_name, $constructor_args );
+ }
+ }
+} else {
+ trait WP_PDO_Proxy_Statement_PHP_Compat {
+ /**
+ * Set the default fetch mode for this statement.
+ *
+ * @param int $mode The fetch mode to set as the default.
+ * @param mixed $args Additional parameters for the default fetch mode.
+ * @return bool True on success, false on failure.
+ */
+ #[ReturnTypeWillChange]
+ public function setFetchMode( $mode, ...$args ): bool {
+ return $this->setDefaultFetchMode( $mode, ...$args );
+ }
+
+ /**
+ * Fetch all remaining rows from the result set.
+ *
+ * @param int $mode The fetch mode to use.
+ * @param mixed $args Additional parameters for the fetch mode.
+ * @return array The result set as an array of rows.
+ */
+ public function fetchAll( $mode = PDO::FETCH_DEFAULT, ...$args ): array {
+ return $this->fetchAllRows( $mode, ...$args );
+ }
+ }
+}
+
+/**
+ * PDOStatement implementation that operates on in-memory data.
+ *
+ * This class implements a complete PDOStatement interface on top of PHP arrays.
+ * It is used for result sets that are composed or transformed in the PHP layer.
+ *
+ * PDO supports the following fetch modes:
+ * - PDO::FETCH_DEFAULT: current default fetch mode (available from PHP 8.0)
+ * - PDO::FETCH_BOTH: default
+ * - PDO::FETCH_NUM: numeric array
+ * - PDO::FETCH_ASSOC: associative array
+ * - PDO::FETCH_NAMED: associative array retaining duplicate columns
+ * - PDO::FETCH_COLUMN: single column value [1 extra arg]
+ * - PDO::FETCH_KEY_PAIR: key-value pair
+ * - PDO::FETCH_OBJ: object (stdClass)
+ * - PDO::FETCH_CLASS: object (custom class) [1-2 extra args]
+ * - PDO::FETCH_INTO: update an exisisting object, can't be used with fetchAll() [1 extra arg]
+ * - PDO::FETCH_LAZY: lazy fetch via PDORow, can't be used with fetchAll()
+ * - PDO::FETCH_BOUND: bind values to PHP variables, can't be used with fetchAll()
+ * - PDO::FETCH_FUNC: custom function, only works with fetchAll(), can't be default [1 extra arg]
+ */
+class WP_PDO_Proxy_Statement extends PDOStatement {
+ use WP_PDO_Proxy_Statement_PHP_Compat;
+
+ /**
+ * The original PDO statement.
+ *
+ * @var PDOStatement
+ */
+ private $statement;
+
+ /**
+ * The number of affected rows.
+ *
+ * @var int|null
+ */
+ private $affected_rows;
+
+ /**
+ * Constructor.
+ *
+ * @param PDOStatement $statement The original PDO statement.
+ * @param int $affected_rows The number of affected rows.
+ */
+ public function __construct(
+ PDOStatement $statement,
+ ?int $affected_rows = null
+ ) {
+ $this->statement = $statement;
+ $this->affected_rows = $affected_rows;
+ }
+
+ /**
+ * Execute a prepared statement.
+ *
+ * @param mixed $params The values to bind to the parameters of the prepared statement.
+ * @return bool True on success, false on failure.
+ */
+ public function execute( $params = null ): bool {
+ return $this->statement->execute( $params );
+ }
+
+ /**
+ * Get the number of columns in the result set.
+ *
+ * @return int The number of columns in the result set.
+ */
+ public function columnCount(): int {
+ return $this->statement->columnCount();
+ }
+
+ /**
+ * Get the number of rows affected by the statement.
+ *
+ * @return int The number of rows affected by the statement.
+ */
+ public function rowCount(): int {
+ return $this->affected_rows ?? $this->statement->rowCount();
+ }
+
+ /**
+ * Fetch the next row from the result set.
+ *
+ * @param int|null $mode The fetch mode. Controls how the row is returned.
+ * Default: PDO::FETCH_DEFAULT (null for PHP < 8.0)
+ * @param int|null $cursorOrientation The cursor orientation. Controls which row is returned.
+ * Default: PDO::FETCH_ORI_NEXT (null for PHP < 8.0)
+ * @param int|null $cursorOffset The cursor offset. Controls which row is returned.
+ * Default: 0 (null for PHP < 8.0)
+ * @return mixed The row data formatted according to the fetch mode;
+ * false if there are no more rows or a failure occurs.
+ */
+ #[ReturnTypeWillChange]
+ public function fetch(
+ $mode = 0, // PDO::FETCH_DEFAULT (available from PHP 8.0)
+ $cursorOrientation = 0,
+ $cursorOffset = 0
+ ) {
+ return $this->statement->fetch( $mode, $cursorOrientation, $cursorOffset );
+ }
+
+ /**
+ * Fetch a single column from the next row of a result set.
+ *
+ * @param int $column The index of the column to fetch (0-indexed).
+ * @return mixed The value of the column; false if there are no more rows.
+ */
+ #[ReturnTypeWillChange]
+ public function fetchColumn( $column = 0 ) {
+ throw new RuntimeException( 'Not implemented' );
+ }
+
+ /**
+ * Fetch the next row as an object.
+ *
+ * @param string $class The name of the class to instantiate.
+ * @param array $constructorArgs The parameters to pass to the class constructor.
+ * @return object The next row as an object.
+ */
+ #[ReturnTypeWillChange]
+ public function fetchObject( $class = 'stdClass', $constructorArgs = array() ) {
+ throw new RuntimeException( 'Not implemented' );
+ }
+
+ /**
+ * Get metadata for a column in a result set.
+ *
+ * @param int $column The index of the column (0-indexed).
+ * @return array|false The column metadata as an associative array,
+ * or false if the column does not exist.
+ */
+ public function getColumnMeta( $column ): array {
+ throw new RuntimeException( 'Not implemented' );
+ }
+
+ /**
+ * Fetch the SQLSTATE associated with the last statement operation.
+ *
+ * @return string|null The SQLSTATE error code (as defined by the ANSI SQL standard),
+ * or null if there is no error.
+ */
+ public function errorCode(): ?string {
+ throw new RuntimeException( 'Not implemented' );
+ }
+
+ /**
+ * Fetch error information associated with the last statement operation.
+ *
+ * @return array The array consists of at least the following fields:
+ * 0: SQLSTATE error code (as defined by the ANSI SQL standard).
+ * 1: Driver-specific error code.
+ * 2: Driver-specific error message.
+ */
+ public function errorInfo(): array {
+ throw new RuntimeException( 'Not implemented' );
+ }
+
+ /**
+ * Get a statement attribute.
+ *
+ * @param int $attribute The attribute to get.
+ * @return mixed The value of the attribute.
+ */
+ #[ReturnTypeWillChange]
+ public function getAttribute( $attribute ) {
+ return $this->statement->getAttribute( $attribute );
+ }
+
+ /**
+ * Set a statement attribute.
+ *
+ * @param int $attribute The attribute to set.
+ * @param mixed $value The value of the attribute.
+ * @return bool True on success, false on failure.
+ */
+ public function setAttribute( $attribute, $value ): bool {
+ return $this->statement->setAttribute( $attribute, $value );
+ }
+
+ /**
+ * Get result set as iterator.
+ *
+ * @return Iterator The iterator for the result set.
+ */
+ public function getIterator(): Iterator {
+ throw new RuntimeException( 'Not implemented' );
+ }
+
+ /**
+ * Advances to the next rowset in a multi-rowset statement handle.
+ *
+ * @return bool True on success, false on failure.
+ */
+ public function nextRowset(): bool {
+ throw new RuntimeException( 'Not implemented' );
+ }
+
+ /**
+ * Closes the cursor, enabling the statement to be executed again.
+ *
+ * @return bool True on success, false on failure.
+ */
+ public function closeCursor(): bool {
+ throw new RuntimeException( 'Not implemented' );
+ }
+
+ /**
+ * Bind a column to a PHP variable.
+ *
+ * @param int|string $column Number of the column (1-indexed) or name of the column in the result set.
+ * @param mixed $var PHP variable to which the column will be bound.
+ * @param int $type Data type of the parameter, specified by the PDO::PARAM_* constants.
+ * @param int $maxLength A hint for pre-allocation.
+ * @param mixed $driverOptions Optional parameters for the driver.
+ * @return bool True on success, false on failure.
+ */
+ public function bindColumn( $column, &$var, $type = null, $maxLength = null, $driverOptions = null ): bool {
+ throw new RuntimeException( 'Not implemented' );
+ }
+
+ /**
+ * Bind a parameter to a PHP variable.
+ *
+ * @param int|string $param Parameter identifier. Either a 1-indexed position of the parameter or a named parameter.
+ * @param mixed $var PHP variable to which the parameter will be bound.
+ * @param int $type Data type of the parameter, specified by the PDO::PARAM_* constants.
+ * @param int $maxLength Length of the data type.
+ * @param mixed $driverOptions Optional parameters for the driver.
+ * @return bool True on success, false on failure.
+ */
+ public function bindParam( $param, &$var, $type = PDO::PARAM_STR, $maxLength = 0, $driverOptions = null ): bool {
+ throw new RuntimeException( 'Not implemented' );
+ }
+
+ /**
+ * Bind a value to a parameter.
+ *
+ * @param int|string $param Parameter identifier. Either a 1-indexed position of the parameter or a named parameter.
+ * @param mixed $value The value to bind to the parameter.
+ * @param int $type Data type of the parameter, specified by the PDO::PARAM_* constants.
+ * @return bool True on success, false on failure.
+ */
+ public function bindValue( $param, $value, $type = PDO::PARAM_STR ): bool {
+ throw new RuntimeException( 'Not implemented' );
+ }
+
+ /**
+ * Dump information about the statement.
+ *
+ * Dupms the SQL query and parameters information.
+ *
+ * @return bool|null Returns null, or false on failure.
+ */
+ public function debugDumpParams(): ?bool {
+ throw new RuntimeException( 'Not implemented' );
+ }
+
+ /**
+ * Fetch all remaining rows from the result set.
+ *
+ * This is used internally by the "WP_PDO_Proxy_Statement_PHP_Compat" trait,
+ * that is defined conditionally based on the current PHP version.
+ *
+ * @param int $mode The fetch mode to use.
+ * @param mixed $args Additional parameters for the fetch mode.
+ * @return array The result set as an array of rows.
+ */
+ private function fetchAllRows( $mode = null, ...$args ): array {
+ return $this->statement->fetchAll( $mode, ...$args );
+ }
+
+ /**
+ * Set the default fetch mode for this statement.
+ *
+ * This is used internally by the "WP_PDO_Proxy_Statement_PHP_Compat" trait,
+ * that is defined conditionally based on the current PHP version.
+ *
+ * @param int $mode The fetch mode to set as the default.
+ * @param mixed $args Additional parameters for the default fetch mode.
+ * @return bool True on success, false on failure.
+ */
+ private function setDefaultFetchMode( $mode, ...$args ): bool {
+ return $this->statement->setFetchMode( $mode, ...$args );
+ }
+}
+
+/**
+ * Polyfill ValueError for PHP < 8.0.
+ */
+if ( PHP_VERSION_ID < 80000 && ! class_exists( ValueError::class ) ) {
+ class ValueError extends Error {
+ }
+}
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-connection.php b/wp-includes/sqlite-ast/class-wp-sqlite-connection.php
index ba607e09..1509a1e6 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-connection.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-connection.php
@@ -91,9 +91,6 @@ public function __construct( array $options ) {
}
$this->pdo->setAttribute( PDO::ATTR_TIMEOUT, $timeout );
- // Return all values (except null) as strings.
- $this->pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
-
// Configure SQLite journal mode.
$journal_mode = $options['journal_mode'] ?? null;
if ( $journal_mode && in_array( $journal_mode, self::SQLITE_JOURNAL_MODES, true ) ) {
diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
index a562ed59..1f427509 100644
--- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
+++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php
@@ -41,6 +41,13 @@ class WP_SQLite_Driver {
*/
private $mysql_on_sqlite_driver;
+ /**
+ * Results of the last emulated query.
+ *
+ * @var mixed
+ */
+ private $last_result;
+
/**
* Constructor.
*
@@ -56,9 +63,19 @@ public function __construct(
string $database,
int $mysql_version = 80038
) {
- $this->mysql_on_sqlite_driver = new WP_PDO_MySQL_On_SQLite( $connection, $database, $mysql_version );
+ $this->mysql_on_sqlite_driver = new WP_PDO_MySQL_On_SQLite(
+ sprintf( 'mysql-on-sqlite:dbname=%s', $database ),
+ null,
+ null,
+ array(
+ 'mysql_version' => $mysql_version,
+ 'pdo' => $connection->get_pdo(),
+ )
+ );
$this->main_db_name = $database;
$this->client_info = $this->mysql_on_sqlite_driver->client_info;
+
+ $connection->get_pdo()->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
}
/**
@@ -139,7 +156,16 @@ public function get_insert_id() {
* @throws WP_SQLite_Driver_Exception When the query execution fails.
*/
public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) {
- return $this->mysql_on_sqlite_driver->query( $query, $fetch_mode, ...$fetch_mode_args );
+ $stmt = $this->mysql_on_sqlite_driver->query( $query, $fetch_mode, ...$fetch_mode_args );
+
+ if ( $stmt->columnCount() > 0 ) {
+ $this->last_result = $stmt->fetchAll( $fetch_mode );
+ } elseif ( $stmt->rowCount() > 0 ) {
+ $this->last_result = $stmt->rowCount();
+ } else {
+ $this->last_result = null;
+ }
+ return $this->last_result;
}
/**
@@ -158,7 +184,7 @@ public function create_parser( string $query ): WP_MySQL_Parser {
* @return mixed
*/
public function get_query_results() {
- return $this->mysql_on_sqlite_driver->get_query_results();
+ return $this->last_result;
}
/**
@@ -167,7 +193,7 @@ public function get_query_results() {
* @return mixed
*/
public function get_last_return_value() {
- return $this->mysql_on_sqlite_driver->get_last_return_value();
+ return $this->last_result;
}
/**
diff --git a/wp-pdo-mysql-on-sqlite.php b/wp-pdo-mysql-on-sqlite.php
index 2061de07..b3d7d24b 100644
--- a/wp-pdo-mysql-on-sqlite.php
+++ b/wp-pdo-mysql-on-sqlite.php
@@ -20,3 +20,4 @@
require_once __DIR__ . '/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-exception.php';
require_once __DIR__ . '/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php';
require_once __DIR__ . '/wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php';
+require_once __DIR__ . '/wp-includes/sqlite-ast/class-wp-pdo-proxy-statement.php';