From 8ca0f1dbbdc32ed82f5402f06f1981466df13648 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Fri, 5 Dec 2025 13:52:41 +0100 Subject: [PATCH 1/4] Rename WP_SQLite_Driver to WP_MySQL_On_SQLite --- ...{class-wp-sqlite-driver.php => class-wp-mysql-on-sqlite.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename wp-includes/sqlite-ast/{class-wp-sqlite-driver.php => class-wp-mysql-on-sqlite.php} (99%) diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php similarity index 99% rename from wp-includes/sqlite-ast/class-wp-sqlite-driver.php rename to wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php index 3a9c13e6..6b3ec6ff 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php @@ -14,7 +14,7 @@ * * The driver requires PDO with the SQLite driver, and the PCRE engine. */ -class WP_SQLite_Driver { +class WP_MySQL_On_SQLite { /** * The path to the MySQL SQL grammar file. */ From 59d175d279b735a18950d4f52d41c0ad61e76cc1 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Fri, 5 Dec 2025 15:19:44 +0100 Subject: [PATCH 2/4] Reintroduce WP_SQLite_Driver as a temporary proxy exposing legacy API --- .../src/Adapter/class-sqlite-adapter.php | 1 + ...Information_Schema_Reconstructor_Tests.php | 2 +- tests/bootstrap.php | 1 + tests/tools/dump-sqlite-query.php | 1 + .../class-wp-sqlite-configurator.php | 12 +- .../class-wp-sqlite-driver-exception.php | 8 +- .../sqlite-ast/class-wp-sqlite-driver.php | 136 ++++++++++++++++++ ...qlite-information-schema-reconstructor.php | 12 +- wp-includes/sqlite/class-wp-sqlite-db.php | 1 + 9 files changed, 157 insertions(+), 17 deletions(-) create mode 100644 wp-includes/sqlite-ast/class-wp-sqlite-driver.php diff --git a/packages/wp-mysql-proxy/src/Adapter/class-sqlite-adapter.php b/packages/wp-mysql-proxy/src/Adapter/class-sqlite-adapter.php index 1df4165c..bae0418e 100644 --- a/packages/wp-mysql-proxy/src/Adapter/class-sqlite-adapter.php +++ b/packages/wp-mysql-proxy/src/Adapter/class-sqlite-adapter.php @@ -22,6 +22,7 @@ require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php'; require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-connection.php'; require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php'; +require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php'; require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; diff --git a/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php b/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php index 6bf93e85..587fbd9e 100644 --- a/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php +++ b/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php @@ -49,7 +49,7 @@ public function setUp(): void { ); $builder = new WP_SQLite_Information_Schema_Builder( - WP_SQLite_Driver::RESERVED_PREFIX, + WP_MySQL_On_SQLite::RESERVED_PREFIX, $this->engine->get_connection() ); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 6acb7baf..b6df9c51 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -16,6 +16,7 @@ require_once __DIR__ . '/../wp-includes/sqlite/class-wp-sqlite-translator.php'; require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-connection.php'; require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-configurator.php'; +require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php'; require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php index 251cd4f7..2964cc4f 100644 --- a/tests/tools/dump-sqlite-query.php +++ b/tests/tools/dump-sqlite-query.php @@ -10,6 +10,7 @@ require_once __DIR__ . '/../../wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-connection.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-configurator.php'; +require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php b/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php index 62d35d5b..8886fdac 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php @@ -14,7 +14,7 @@ class WP_SQLite_Configurator { /** * The SQLite driver instance. * - * @var WP_SQLite_Driver + * @var WP_MySQL_On_SQLite */ private $driver; @@ -35,11 +35,11 @@ class WP_SQLite_Configurator { /** * Constructor. * - * @param WP_SQLite_Driver $driver The SQLite driver instance. + * @param WP_MySQL_On_SQLite $driver The SQLite driver instance. * @param WP_SQLite_Information_Schema_Builder $schema_builder The information schema builder instance. */ public function __construct( - WP_SQLite_Driver $driver, + WP_MySQL_On_SQLite $driver, WP_SQLite_Information_Schema_Builder $schema_builder ) { $this->driver = $driver; @@ -100,7 +100,7 @@ private function ensure_global_variables_table(): void { sprintf( 'CREATE TABLE IF NOT EXISTS %s (name TEXT PRIMARY KEY, value TEXT)', $this->driver->get_connection()->quote_identifier( - WP_SQLite_Driver::GLOBAL_VARIABLES_TABLE_NAME + WP_MySQL_On_SQLite::GLOBAL_VARIABLES_TABLE_NAME ) ) ); @@ -260,11 +260,11 @@ private function save_current_driver_version(): void { sprintf( 'INSERT INTO %s (name, value) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET value = ?', $this->driver->get_connection()->quote_identifier( - WP_SQLite_Driver::GLOBAL_VARIABLES_TABLE_NAME + WP_MySQL_On_SQLite::GLOBAL_VARIABLES_TABLE_NAME ) ), array( - WP_SQLite_Driver::DRIVER_VERSION_VARIABLE_NAME, + WP_MySQL_On_SQLite::DRIVER_VERSION_VARIABLE_NAME, SQLITE_DRIVER_VERSION, SQLITE_DRIVER_VERSION, ) diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php index 91918bdf..1c189bdd 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php @@ -4,20 +4,20 @@ class WP_SQLite_Driver_Exception extends PDOException { /** * The SQLite driver that originated the exception. * - * @var WP_SQLite_Driver + * @var WP_MySQL_On_SQLite */ private $driver; /** * Constructor. * - * @param WP_SQLite_Driver $driver The SQLite driver that originated the exception. + * @param WP_MySQL_On_SQLite $driver The SQLite driver that originated the exception. * @param string $message The exception message. * @param int|string $code The exception code. In PDO, it can be a string with value of SQLSTATE. * @param Throwable|null $previous The previous throwable used for the exception chaining. */ public function __construct( - WP_SQLite_Driver $driver, + WP_MySQL_On_SQLite $driver, string $message, $code = 0, ?Throwable $previous = null @@ -27,7 +27,7 @@ public function __construct( $this->driver = $driver; } - public function getDriver(): WP_SQLite_Driver { + public function getDriver(): WP_MySQL_On_SQLite { return $this->driver; } } diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php new file mode 100644 index 00000000..2c7f5c3a --- /dev/null +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -0,0 +1,136 @@ +mysql_on_sqlite_driver = new WP_MySQL_On_SQLite( $connection, $database, $mysql_version ); + $this->main_db_name = $database; + $this->client_info = $this->mysql_on_sqlite_driver->client_info; + } + + public function get_connection(): WP_SQLite_Connection { + return $this->mysql_on_sqlite_driver->get_connection(); + } + + public function get_sqlite_version(): string { + return $this->mysql_on_sqlite_driver->get_sqlite_version(); + } + + public function get_saved_driver_version(): string { + return $this->mysql_on_sqlite_driver->get_saved_driver_version(); + } + + public function is_sql_mode_active( string $mode ): bool { + return $this->mysql_on_sqlite_driver->is_sql_mode_active( $mode ); + } + + public function get_last_mysql_query(): ?string { + return $this->mysql_on_sqlite_driver->get_last_mysql_query(); + } + + public function get_last_sqlite_queries(): array { + return $this->mysql_on_sqlite_driver->get_last_sqlite_queries(); + } + + /** @return int|string */ + public function get_insert_id() { + return $this->mysql_on_sqlite_driver->get_insert_id(); + } + + /** + * @param string $query Full SQL statement string. + * @param int $fetch_mode PDO fetch mode. Default is PDO::FETCH_OBJ. + * @param array ...$fetch_mode_args Additional fetch mode arguments. + * + * @return mixed Return value, depending on the query type. + * + * @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 ); + } + + public function create_parser( string $query ): WP_MySQL_Parser { + return $this->mysql_on_sqlite_driver->create_parser( $query ); + } + + /** + * @return mixed + */ + public function get_query_results() { + return $this->mysql_on_sqlite_driver->get_query_results(); + } + + /** + * @return mixed + */ + public function get_last_return_value() { + return $this->mysql_on_sqlite_driver->get_last_return_value(); + } + + public function get_last_column_count(): int { + return $this->mysql_on_sqlite_driver->get_last_column_count(); + } + + public function get_last_column_meta(): array { + return $this->mysql_on_sqlite_driver->get_last_column_meta(); + } + + public function execute_sqlite_query( string $sql, array $params = array() ): PDOStatement { + return $this->mysql_on_sqlite_driver->execute_sqlite_query( $sql, $params ); + } + + public function begin_transaction(): void { + $this->mysql_on_sqlite_driver->begin_transaction(); + } + + public function commit(): void { + $this->mysql_on_sqlite_driver->commit(); + } + + public function rollback(): void { + $this->mysql_on_sqlite_driver->rollback(); + } + + /** + * Proxy also this private method, as it is used in tests. + */ + private function quote_mysql_utf8_string_literal( string $utf8_literal ): string { + $closure = function ( string $utf8_literal ) { + return $this->quote_mysql_utf8_string_literal( $utf8_literal ); + }; + return $closure->call( $this->mysql_on_sqlite_driver, $utf8_literal ); + } + + /** + * Proxy also the private property "$main_db_name", as it is used in tests. + */ + public function __set( string $name, $value ): void { + if ( 'main_db_name' === $name ) { + $closure = function ( string $value ) { + $this->main_db_name = $value; + }; + $closure->call( $this->mysql_on_sqlite_driver, $value ); + } + } +} diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php index c739234b..bf7a9780 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php @@ -19,7 +19,7 @@ class WP_SQLite_Information_Schema_Reconstructor { /** * The SQLite driver instance. * - * @var WP_SQLite_Driver + * @var WP_MySQL_On_SQLite */ private $driver; @@ -40,11 +40,11 @@ class WP_SQLite_Information_Schema_Reconstructor { /** * Constructor. * - * @param WP_SQLite_Driver $driver The SQLite driver instance. + * @param WP_MySQL_On_SQLite $driver The SQLite driver instance. * @param WP_SQLite_Information_Schema_Builder $schema_builder The information schema builder instance. */ public function __construct( - WP_SQLite_Driver $driver, + $driver, WP_SQLite_Information_Schema_Builder $schema_builder ) { $this->driver = $driver; @@ -137,7 +137,7 @@ private function get_sqlite_table_names(): array { array( '_mysql_data_types_cache', 'sqlite\_%', - str_replace( '_', '\_', WP_SQLite_Driver::RESERVED_PREFIX ) . '%', + str_replace( '_', '\_', WP_MySQL_On_SQLite::RESERVED_PREFIX ) . '%', ) )->fetchAll( PDO::FETCH_COLUMN ); } @@ -692,9 +692,9 @@ private function get_mysql_column_type( string $column_type ): string { /** * Format a MySQL UTF-8 string literal for output in a CREATE TABLE statement. * - * See WP_SQLite_Driver::quote_mysql_utf8_string_literal(). + * See WP_MySQL_On_SQLite::quote_mysql_utf8_string_literal(). * - * TODO: This is a copy of WP_SQLite_Driver::quote_mysql_utf8_string_literal(). + * TODO: This is a copy of WP_MySQL_On_SQLite::quote_mysql_utf8_string_literal(). * We may consider extracing it to reusable MySQL helpers. * * @param string $utf8_literal The UTF-8 string literal to escape. diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php index 1bc00aaa..77605409 100644 --- a/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/wp-includes/sqlite/class-wp-sqlite-db.php @@ -324,6 +324,7 @@ public function db_connect( $allow_bail = true ) { require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-connection.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-configurator.php'; + require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; From c3276c8b4a8e7683a643c6c6feace77d43a1775a Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Mon, 8 Dec 2025 16:38:45 +0100 Subject: [PATCH 3/4] Add basic PDOStatement implementation --- .../src/Adapter/class-sqlite-adapter.php | 1 + tests/bootstrap.php | 1 + tests/tools/dump-sqlite-query.php | 1 + .../sqlite-ast/class-wp-pdo-statement.php | 70 +++++++++++++++++++ wp-includes/sqlite/class-wp-sqlite-db.php | 1 + 5 files changed, 74 insertions(+) create mode 100644 wp-includes/sqlite-ast/class-wp-pdo-statement.php diff --git a/packages/wp-mysql-proxy/src/Adapter/class-sqlite-adapter.php b/packages/wp-mysql-proxy/src/Adapter/class-sqlite-adapter.php index bae0418e..68e38260 100644 --- a/packages/wp-mysql-proxy/src/Adapter/class-sqlite-adapter.php +++ b/packages/wp-mysql-proxy/src/Adapter/class-sqlite-adapter.php @@ -23,6 +23,7 @@ require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-connection.php'; require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php'; require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php'; +require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-pdo-statement.php'; require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index b6df9c51..c4af5c10 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -17,6 +17,7 @@ require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-connection.php'; require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-configurator.php'; require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php'; +require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-pdo-statement.php'; require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php index 2964cc4f..e598cd2b 100644 --- a/tests/tools/dump-sqlite-query.php +++ b/tests/tools/dump-sqlite-query.php @@ -11,6 +11,7 @@ require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-connection.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-configurator.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php'; +require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-pdo-statement.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; diff --git a/wp-includes/sqlite-ast/class-wp-pdo-statement.php b/wp-includes/sqlite-ast/class-wp-pdo-statement.php new file mode 100644 index 00000000..9893cc4c --- /dev/null +++ b/wp-includes/sqlite-ast/class-wp-pdo-statement.php @@ -0,0 +1,70 @@ +stmt = $stmt; + } + + public function execute( ?array $params = null ): bool { + // TODO: Implement. + return true; + } + + public function columnCount(): int { + return $this->stmt->columnCount(); + } + + public function rowCount(): int { + return $this->stmt->rowCount(); + } + + #[ReturnTypeWillChange] + public function fetch( int $mode = PDO::FETCH_DEFAULT, int $cursorOrientation = PDO::FETCH_ORI_NEXT, int $cursorOffset = 0 ) { + } + + public function fetchAll( int $mode = PDO::FETCH_DEFAULT, ...$args ): array { + } + + #[ReturnTypeWillChange] + public function fetchColumn( int $column = 0 ) { + return $this->stmt->fetchColumn( $column ); + } + + #[ReturnTypeWillChange] + public function fetchObject( ?string $class = 'stdClass', array $constructorArgs = array() ) { + } + + public function getColumnMeta( int $column ): array { + return array(); + } + + public function errorCode(): ?string { + return '00000'; + } + + public function errorInfo(): array { + return array( '00000', '00000', '00000' ); + } + + // TODO: + // public function bindColumn() + // public function bindParam() + // public function bindValue() + // public function closeCursor() + // public function debugDumpParams() + // public function setFetchMode() + // public function setAttribute() + // public function getAttribute() + // public function getIterator() + // public function nextRowset() +} diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php index 77605409..83bb5fbc 100644 --- a/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/wp-includes/sqlite/class-wp-sqlite-db.php @@ -325,6 +325,7 @@ public function db_connect( $allow_bail = true ) { require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-connection.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-configurator.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php'; + require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-pdo-statement.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; From 3bbdbf19e4d54120935d51174c8c2c7197076325 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Mon, 8 Dec 2025 16:56:09 +0100 Subject: [PATCH 4/4] wip --- tests/WP_MySQL_On_SQLite_PDO_API_Tests.php | 63 +++ .../sqlite-ast/class-wp-mysql-on-sqlite.php | 511 ++++++++++-------- .../sqlite-ast/class-wp-pdo-statement.php | 3 + .../sqlite-ast/class-wp-sqlite-connection.php | 7 + .../sqlite-ast/class-wp-sqlite-driver.php | 29 +- 5 files changed, 396 insertions(+), 217 deletions(-) create mode 100644 tests/WP_MySQL_On_SQLite_PDO_API_Tests.php diff --git a/tests/WP_MySQL_On_SQLite_PDO_API_Tests.php b/tests/WP_MySQL_On_SQLite_PDO_API_Tests.php new file mode 100644 index 00000000..f2ae5fc4 --- /dev/null +++ b/tests/WP_MySQL_On_SQLite_PDO_API_Tests.php @@ -0,0 +1,63 @@ +driver = new WP_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=WordPress;' ); + } + + public function test_connection(): void { + $driver = new WP_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=WordPress;' ); + $this->assertInstanceOf( PDO::class, $driver ); + } + + public function test_query(): void { + $result = $this->driver->query( 'SELECT 1' ); + $this->assertInstanceOf( PDOStatement::class, $result ); + $this->assertEquals( 1, $result->fetchColumn() ); + } + + public function test_begin_transaction(): void { + $result = $this->driver->beginTransaction(); + $this->assertTrue( $result ); + } + + public function test_begin_transaction_already_active(): void { + $this->driver->beginTransaction(); + + $this->expectException( PDOException::class ); + $this->expectExceptionMessage( 'There is already an active transaction' ); + $this->expectExceptionCode( 0 ); + $this->driver->beginTransaction(); + } + + public function test_commit(): void { + $this->driver->beginTransaction(); + $result = $this->driver->commit(); + $this->assertTrue( $result ); + } + + public function test_commit_no_active_transaction(): void { + $this->expectException( PDOException::class ); + $this->expectExceptionMessage( 'There is no active transaction' ); + $this->expectExceptionCode( 0 ); + $this->driver->commit(); + } + + public function test_rollback(): void { + $this->driver->beginTransaction(); + $result = $this->driver->rollBack(); + $this->assertTrue( $result ); + } + + public function test_rollback_no_active_transaction(): void { + $this->expectException( PDOException::class ); + $this->expectExceptionMessage( 'There is no active transaction' ); + $this->expectExceptionCode( 0 ); + $this->driver->rollBack(); + } +} diff --git a/wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php b/wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php index 6b3ec6ff..efd13294 100644 --- a/wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php +++ b/wp-includes/sqlite-ast/class-wp-mysql-on-sqlite.php @@ -14,7 +14,7 @@ * * The driver requires PDO with the SQLite driver, and the PCRE engine. */ -class WP_MySQL_On_SQLite { +class WP_MySQL_On_SQLite extends PDO { /** * The path to the MySQL SQL grammar file. */ @@ -443,6 +443,11 @@ class WP_MySQL_On_SQLite { */ private $information_schema_builder; + /** + * @var PDOStatement + */ + private $last_result_statement; + /** * Last executed MySQL query. * @@ -493,11 +498,11 @@ class WP_MySQL_On_SQLite { private $is_readonly; /** - * Transaction nesting level of the executed SQLite queries. + * Whether the current MySQL query emulation is wrapped in a transaction. * * @var int */ - private $transaction_level = 0; + private $is_wrapped_in_transaction = false; /** * Whether a MySQL table lock is active. @@ -561,24 +566,47 @@ class WP_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; + // 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' ); + } + + $args = array(); + foreach ( explode( ';', $dsn_parts[1] ) as $arg ) { + $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. + $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 ) { @@ -666,6 +694,149 @@ function ( string $sql, array $params ) { ); } + /** + * PDO API: Translate and execute a MySQL query in SQLite. + * + * A single MySQL query can be translated into zero or more SQLite queries. + * + * @param string $query Full SQL statement string. + * @param int $fetch_mode PDO fetch mode. Default is PDO::FETCH_OBJ. + * @param array ...$fetch_mode_args Additional fetch mode arguments. + * + * @return mixed Return value, depending on the query type. + * + * @throws WP_SQLite_Driver_Exception When the query execution fails. + */ + #[ReturnTypeWillChange] + public function query( string $query, ?int $fetch_mode = PDO::FETCH_COLUMN, ...$fetch_mode_args ) { + $this->flush(); + $this->pdo_fetch_mode = $fetch_mode; + $this->last_mysql_query = $query; + + try { + // Parse the MySQL query. + $parser = $this->create_parser( $query ); + $parser->next_query(); + $ast = $parser->get_query_ast(); + if ( null === $ast ) { + throw $this->new_driver_exception( 'Failed to parse the MySQL query.' ); + } + + if ( $parser->next_query() ) { + throw $this->new_driver_exception( 'Multi-query is not supported.' ); + } + + /* + * Determine if we need to wrap the translated queries in a transaction. + * + * [GRAMMAR] + * query: + * EOF + * | (simpleStatement | beginWork) (SEMICOLON_SYMBOL EOF? | EOF) + */ + $child_node = $ast->get_first_child_node(); + if ( + null === $child_node + || 'beginWork' === $child_node->rule_name + || $child_node->has_child_node( 'transactionOrLockingStatement' ) + ) { + $wrap_in_transaction = false; + } else { + $wrap_in_transaction = true; + } + + if ( $wrap_in_transaction ) { + $this->begin_wrapper_transaction(); + } + + $this->execute_mysql_query( $ast ); + + if ( $wrap_in_transaction ) { + $this->commit_wrapper_transaction(); + } + return new WP_PDO_Statement( $this->last_result_statement ); + } catch ( Throwable $e ) { + try { + $this->connection->prepare( 'ROLLBACK' )->execute(); + } catch ( Throwable $rollback_exception ) { + // Ignore rollback errors. + } + if ( $e instanceof WP_SQLite_Driver_Exception ) { + throw $e; + } elseif ( $e instanceof WP_SQLite_Information_Schema_Exception ) { + throw $this->convert_information_schema_exception( $e ); + } + throw $this->new_driver_exception( $e->getMessage(), $e->getCode(), $e ); + } + } + + /** + * PDO API: Begin a new transaction or nested transaction. + * + * @return bool True on success, false on failure. + */ + public function beginTransaction(): bool { + if ( $this->is_wrapped_in_transaction ) { + $savepoint_name = $this->get_internal_savepoint_name(); + $result = $this->connection->prepare( sprintf( 'SAVEPOINT %s', $savepoint_name ) ); + } else { + $result = $this->connection->prepare( 'BEGIN IMMEDIATE' ); + } + + try { + return $result->execute(); + } catch ( PDOException $e ) { + // Convert SQLite error to MySQL error. + if ( str_contains( $e->getMessage(), 'cannot start a transaction within a transaction' ) ) { + throw $this->new_driver_exception( 'There is already an active transaction' ); + } + } + } + + /** + * PDO API: Commit the current transaction or nested transaction. + * + * @return bool True on success, false on failure. + */ + public function commit(): bool { + if ( $this->is_wrapped_in_transaction ) { + $savepoint_name = $this->get_internal_savepoint_name(); + $result = $this->connection->prepare( sprintf( 'RELEASE SAVEPOINT %s', $savepoint_name ) ); + } else { + $result = $this->connection->prepare( 'COMMIT' ); + } + try { + return $result->execute(); + } catch ( PDOException $e ) { + // Convert SQLite error to MySQL error. + if ( str_contains( $e->getMessage(), 'cannot commit - no transaction is active' ) ) { + throw $this->new_driver_exception( 'There is no active transaction' ); + } + } + } + + /** + * PDO API: Rollback the current transaction or nested transaction. + * + * @return bool True on success, false on failure. + */ + public function rollBack(): bool { + if ( $this->is_wrapped_in_transaction ) { + $savepoint_name = $this->get_internal_savepoint_name(); + $result = $this->connection->prepare( sprintf( 'ROLLBACK TO SAVEPOINT %s', $savepoint_name ) ); + } else { + $result = $this->connection->prepare( 'ROLLBACK' ); + } + try { + return $result->execute(); + } catch ( PDOException $e ) { + // Convert SQLite error to MySQL error. + if ( str_contains( $e->getMessage(), 'cannot rollback - no transaction is active' ) ) { + throw $this->new_driver_exception( 'There is no active transaction' ); + } + } + } + /** * Get the SQLite connection instance. * @@ -753,86 +924,6 @@ public function get_insert_id() { return $last_insert_id; } - /** - * Translate and execute a MySQL query in SQLite. - * - * A single MySQL query can be translated into zero or more SQLite queries. - * - * @param string $query Full SQL statement string. - * @param int $fetch_mode PDO fetch mode. Default is PDO::FETCH_OBJ. - * @param array ...$fetch_mode_args Additional fetch mode arguments. - * - * @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 ) { - $this->flush(); - $this->pdo_fetch_mode = $fetch_mode; - $this->last_mysql_query = $query; - - try { - // Parse the MySQL query. - $parser = $this->create_parser( $query ); - $parser->next_query(); - $ast = $parser->get_query_ast(); - if ( null === $ast ) { - throw $this->new_driver_exception( 'Failed to parse the MySQL query.' ); - } - - if ( $parser->next_query() ) { - throw $this->new_driver_exception( 'Multi-query is not supported.' ); - } - - /* - * Determine if we need to wrap the translated queries in a transaction. - * - * [GRAMMAR] - * query: - * EOF - * | (simpleStatement | beginWork) (SEMICOLON_SYMBOL EOF? | EOF) - */ - $child_node = $ast->get_first_child_node(); - if ( - null === $child_node - || 'beginWork' === $child_node->rule_name - || $child_node->has_child_node( 'transactionOrLockingStatement' ) - ) { - $wrap_in_transaction = false; - } else { - $wrap_in_transaction = true; - } - - if ( $wrap_in_transaction ) { - $this->begin_transaction(); - } - - $this->execute_mysql_query( $ast ); - - if ( $wrap_in_transaction ) { - $this->commit(); - } - return $this->last_return_value; - } catch ( Throwable $e ) { - try { - $this->rollback(); - } catch ( Throwable $rollback_exception ) { - // Ignore rollback errors. - } - if ( $e instanceof WP_SQLite_Driver_Exception ) { - throw $e; - } elseif ( $e instanceof WP_SQLite_Information_Schema_Exception ) { - throw $this->convert_information_schema_exception( $e ); - } - throw $this->new_driver_exception( $e->getMessage(), $e->getCode(), $e ); - } - } - /** * Tokenize a MySQL query and initialize a parser. * @@ -1049,92 +1140,6 @@ public function execute_sqlite_query( string $sql, array $params = array() ): PD return $this->connection->query( $sql, $params ); } - /** - * Begin a new transaction or nested transaction. - */ - public function begin_transaction(): void { - if ( 0 === $this->transaction_level ) { - /* - * When we're executing a statement that will write to the database, - * we need to use "BEGIN IMMEDIATE" to open a write transaction. - * - * This is needed to avoid the "database is locked" error (SQLITE_BUSY) - * when SQLite can't upgrade a read transaction to a write transaction, - * because another connection is modifying the database. - * - * From the SQLite documentation: - * - * ## Read transactions versus write transactions - * - * If a write statement occurs while a read transaction is active, - * then the read transaction is upgraded to a write transaction if - * possible. If some other database connection has already modified - * the database or is already in the process of modifying the database, - * then upgrading to a write transaction is not possible and the write - * statement will fail with SQLITE_BUSY. - * - * ## DEFERRED, IMMEDIATE, and EXCLUSIVE transactions - * - * Transactions can be DEFERRED, IMMEDIATE, or EXCLUSIVE. The default - * transaction behavior is DEFERRED. - * - * DEFERRED means that the transaction does not actually start until - * the database is first accessed. - * - * IMMEDIATE causes the database connection to start a new write - * immediately, without waiting for a write statement. The BEGIN - * IMMEDIATE might fail with SQLITE_BUSY if another write transaction - * is already active on another database connection. - * - * See: - * - https://www.sqlite.org/lang_transaction.html - * - https://www.sqlite.org/rescode.html#busy - * - * For better performance, we could also consider opening the write - * transaction later in the session - just before the first write. - */ - $this->execute_sqlite_query( $this->is_readonly ? 'BEGIN' : 'BEGIN IMMEDIATE' ); - } else { - $savepoint_name = $this->get_internal_savepoint_name( $this->transaction_level ); - $this->execute_sqlite_query( sprintf( 'SAVEPOINT %s', $savepoint_name ) ); - } - ++$this->transaction_level; - } - - /** - * Commit the current transaction or nested transaction. - */ - public function commit(): void { - if ( 0 === $this->transaction_level ) { - return; - } - - --$this->transaction_level; - if ( 0 === $this->transaction_level ) { - $this->execute_sqlite_query( 'COMMIT' ); - } else { - $savepoint_name = $this->get_internal_savepoint_name( $this->transaction_level ); - $this->execute_sqlite_query( sprintf( 'RELEASE SAVEPOINT %s', $savepoint_name ) ); - } - } - - /** - * Rollback the current transaction or nested transaction. - */ - public function rollback(): void { - if ( 0 === $this->transaction_level ) { - return; - } - - --$this->transaction_level; - if ( 0 === $this->transaction_level ) { - $this->execute_sqlite_query( 'ROLLBACK' ); - } else { - $savepoint_name = $this->get_internal_savepoint_name( $this->transaction_level ); - $this->execute_sqlite_query( sprintf( 'ROLLBACK TO SAVEPOINT %s', $savepoint_name ) ); - } - } - /** * Translate and execute a MySQL query in SQLite. * @@ -1162,7 +1167,7 @@ private function execute_mysql_query( WP_Parser_Node $node ): void { } if ( 'beginWork' === $children[0]->rule_name ) { - $this->begin_transaction(); + $this->beginTransaction(); return; } @@ -1246,8 +1251,9 @@ 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 ); + $query = $this->translate( $node ); + $stmt = $this->execute_sqlite_query( $query ); + $this->last_result_statement = $stmt; $this->set_result_from_affected_rows(); } break; @@ -1291,6 +1297,68 @@ private function execute_mysql_query( WP_Parser_Node $node ): void { } } + private function begin_wrapper_transaction(): void { + if ( $this->is_wrapped_in_transaction ) { + return; + } + + /* + * When we're executing a statement that will write to the database, + * we need to use "BEGIN IMMEDIATE" to open a write transaction. + * + * This is needed to avoid the "database is locked" error (SQLITE_BUSY) + * when SQLite can't upgrade a read transaction to a write transaction, + * because another connection is modifying the database. + * + * From the SQLite documentation: + * + * ## Read transactions versus write transactions + * + * If a write statement occurs while a read transaction is active, + * then the read transaction is upgraded to a write transaction if + * possible. If some other database connection has already modified + * the database or is already in the process of modifying the database, + * then upgrading to a write transaction is not possible and the write + * statement will fail with SQLITE_BUSY. + * + * ## DEFERRED, IMMEDIATE, and EXCLUSIVE transactions + * + * Transactions can be DEFERRED, IMMEDIATE, or EXCLUSIVE. The default + * transaction behavior is DEFERRED. + * + * DEFERRED means that the transaction does not actually start until + * the database is first accessed. + * + * IMMEDIATE causes the database connection to start a new write + * immediately, without waiting for a write statement. The BEGIN + * IMMEDIATE might fail with SQLITE_BUSY if another write transaction + * is already active on another database connection. + * + * See: + * - https://www.sqlite.org/lang_transaction.html + * - https://www.sqlite.org/rescode.html#busy + * + * For better performance, we could also consider opening the write + * transaction later in the session - just before the first write. + */ + $result = $this->connection->prepare( $this->is_readonly ? 'BEGIN' : 'BEGIN IMMEDIATE' ); + if ( false === $result->execute() ) { + throw $this->new_driver_exception( 'Failed to begin transaction.' ); + } + $this->is_wrapped_in_transaction = true; + } + + private function commit_wrapper_transaction(): void { + if ( false === $this->is_wrapped_in_transaction ) { + return; + } + $result = $this->connection->prepare( 'COMMIT' ); + if ( false === $result->execute() ) { + throw $this->new_driver_exception( 'Failed to commit transaction.' ); + } + $this->is_wrapped_in_transaction = false; + } + /** * Execute a MySQL transaction or locking statement in SQLite. * @@ -1305,7 +1373,7 @@ private function execute_transaction_or_locking_statement( WP_Parser_Node $node case 'transactionStatement': // START TRANSACTION. if ( WP_MySQL_Lexer::START_SYMBOL === $token->id ) { - $this->begin_transaction(); + $this->beginTransaction(); return; } @@ -1376,8 +1444,8 @@ private function execute_transaction_or_locking_statement( WP_Parser_Node $node } // Start a transaction when no top-level transaction is active. - if ( 0 === $this->transaction_level ) { - $this->begin_transaction(); + if ( ! $this->is_wrapped_in_transaction ) { + $this->beginTransaction(); $this->table_lock_active = true; } return; @@ -1392,7 +1460,7 @@ private function execute_transaction_or_locking_statement( WP_Parser_Node $node ) ) { // Commit the transaction when created by the LOCK statement. - if ( 1 === $this->transaction_level && $this->table_lock_active ) { + if ( $this->table_lock_active ) { $this->commit(); $this->table_lock_active = false; } @@ -1483,14 +1551,15 @@ private function execute_select_statement( WP_Parser_Node $node ): void { } // Execute the query. - $stmt = $this->execute_sqlite_query( $query ); + $stmt = $this->execute_sqlite_query( $query ); + $this->last_result_statement = $stmt; // 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->set_results_from_fetched_data( + // $stmt->fetchAll( $this->pdo_fetch_mode ) + // ); } /** @@ -1539,8 +1608,9 @@ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): vo $parts[] = $this->translate( $child ); } } - $query = implode( ' ', $parts ); - $this->execute_sqlite_query( $query ); + $query = implode( ' ', $parts ); + $stmt = $this->execute_sqlite_query( $query ); + $this->last_result_statement = $stmt; $this->set_result_from_affected_rows(); } @@ -1777,7 +1847,8 @@ private function execute_update_statement( WP_Parser_Node $node ): void { ); $query = implode( ' ', array_filter( $parts ) ); - $this->execute_sqlite_query( $query ); + $stmt = $this->execute_sqlite_query( $query ); + $this->last_result_statement = $stmt; $this->set_result_from_affected_rows(); } @@ -1875,6 +1946,7 @@ private function execute_delete_statement( WP_Parser_Node $node ): void { } $this->set_result_from_affected_rows( $rows ); + $this->last_result_statement = null; // TODO: Construct a new PDOStatement object. return; } @@ -1886,8 +1958,9 @@ 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 ); + $query = $this->translate( $node ); + $stmt = $this->execute_sqlite_query( $query ); + $this->last_result_statement = $stmt; $this->set_result_from_affected_rows(); } @@ -1940,6 +2013,7 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void { if ( $table_exists ) { $this->set_result_from_affected_rows( 0 ); + $this->last_result_statement = null; // TODO: Construct a new PDOStatement object. return; } } @@ -2103,7 +2177,7 @@ private function execute_truncate_table_statement( WP_Parser_Node $node ): void throw $this->new_access_denied_to_information_schema_exception(); } - $this->execute_sqlite_query( + $stmt = $this->execute_sqlite_query( sprintf( 'DELETE FROM %s', $this->quote_sqlite_identifier( $table_name ) ) ); try { @@ -2115,6 +2189,7 @@ private function execute_truncate_table_statement( WP_Parser_Node $node ): void throw $e; } } + $this->last_result_statement = $stmt; $this->set_result_from_affected_rows(); } @@ -2259,6 +2334,7 @@ private function execute_show_statement( WP_Parser_Node $node ): void { $sql = $this->get_mysql_create_table_statement( $table_is_temporary, $table_name ); if ( null === $sql ) { $this->set_results_from_fetched_data( array() ); + $this->last_result_statement = null; // TODO: Construct a new PDOStatement object. } else { $this->set_results_from_fetched_data( array( @@ -2267,6 +2343,7 @@ private function execute_show_statement( WP_Parser_Node $node ): void { ), ) ); + $this->last_result_statement = null; // TODO: Construct a new PDOStatement object. } $this->last_column_meta = array( @@ -2305,7 +2382,8 @@ private function execute_show_statement( WP_Parser_Node $node ): void { ), ) ); - $this->last_column_meta = array( + $this->last_result_statement = null; // TODO: Construct a new PDOStatement object. + $this->last_column_meta = array( array( 'native_type' => 'STRING', 'pdo_type' => PDO::PARAM_STR, @@ -2389,6 +2467,7 @@ private function execute_show_collation_statement( WP_Parser_Node $node ): void ) ); $this->store_last_column_meta_from_statement( $stmt ); + $this->last_result_statement = $stmt; $this->set_results_from_fetched_data( $stmt->fetchAll( PDO::FETCH_OBJ ) ); } @@ -2421,7 +2500,8 @@ private function execute_show_databases_statement( WP_Parser_Node $node ): void ); $this->store_last_column_meta_from_statement( $stmt ); - $databases = $stmt->fetchAll( PDO::FETCH_OBJ ); + $this->last_result_statement = $stmt; + $databases = $stmt->fetchAll( PDO::FETCH_OBJ ); $this->set_results_from_fetched_data( $databases ); } @@ -2507,7 +2587,8 @@ private function execute_show_index_statement( WP_Parser_Node $node ): void { ); $this->store_last_column_meta_from_statement( $stmt ); - $index_info = $stmt->fetchAll( PDO::FETCH_OBJ ); + $index_info = $stmt->fetchAll( PDO::FETCH_OBJ ); + $this->last_result_statement = $stmt; $this->set_results_from_fetched_data( $index_info ); } @@ -2570,7 +2651,8 @@ private function execute_show_table_status_statement( WP_Parser_Node $node ): vo ); $this->store_last_column_meta_from_statement( $stmt ); - $table_info = $stmt->fetchAll( PDO::FETCH_OBJ ); + $table_info = $stmt->fetchAll( PDO::FETCH_OBJ ); + $this->last_result_statement = $stmt; if ( false === $table_info ) { $this->set_results_from_fetched_data( array() ); } @@ -2622,7 +2704,8 @@ private function execute_show_tables_statement( WP_Parser_Node $node ): void { ); $this->store_last_column_meta_from_statement( $stmt ); - $table_info = $stmt->fetchAll( PDO::FETCH_OBJ ); + $table_info = $stmt->fetchAll( PDO::FETCH_OBJ ); + $this->last_result_statement = $stmt; if ( false === $table_info ) { $this->set_results_from_fetched_data( array() ); } @@ -2695,7 +2778,8 @@ private function execute_show_columns_statement( WP_Parser_Node $node ): void { ); $this->store_last_column_meta_from_statement( $stmt ); - $column_info = $stmt->fetchAll( PDO::FETCH_OBJ ); + $column_info = $stmt->fetchAll( PDO::FETCH_OBJ ); + $this->last_result_statement = $stmt; if ( false === $column_info ) { $this->set_results_from_fetched_data( array() ); } @@ -2734,7 +2818,8 @@ private function execute_describe_statement( WP_Parser_Node $node ): void { ); $this->store_last_column_meta_from_statement( $stmt ); - $column_info = $stmt->fetchAll( PDO::FETCH_OBJ ); + $column_info = $stmt->fetchAll( PDO::FETCH_OBJ ); + $this->last_result_statement = $stmt; $this->set_results_from_fetched_data( $column_info ); } @@ -3110,6 +3195,7 @@ private function execute_administration_statement( WP_Parser_Node $node ): void ), ); $this->set_results_from_fetched_data( $results ); + $this->last_result_statement = null; // TODO: Construct a new PDOStatement object. } /** @@ -5867,11 +5953,10 @@ private function get_sqlite_index_name( string $mysql_table_name, string $mysql_ * Internal savepoints are used to emulate MySQL transactions that are run * inside a wrapping SQLite transaction, as transactions can't be nested. * - * @param int $level The transaction nesting level. - * @return string The internal savepoint name. + * @return string The internal savepoint name. */ - private function get_internal_savepoint_name( int $level ): string { - return sprintf( '%ssavepoint_%d', self::RESERVED_PREFIX, $level ); + private function get_internal_savepoint_name(): string { + return sprintf( '%ssavepoint', self::RESERVED_PREFIX ); } /** @@ -5994,12 +6079,14 @@ private function quote_mysql_utf8_string_literal( string $utf8_literal ): string * Clear the state of the driver. */ private function flush(): void { - $this->last_mysql_query = ''; - $this->last_sqlite_queries = array(); - $this->last_result = null; - $this->last_return_value = null; - $this->last_column_meta = array(); - $this->is_readonly = false; + $this->last_result_statement = null; + $this->last_mysql_query = ''; + $this->last_sqlite_queries = array(); + $this->last_result = null; + $this->last_return_value = null; + $this->last_column_meta = array(); + $this->is_readonly = false; + $this->is_wrapped_in_transaction = false; } /** diff --git a/wp-includes/sqlite-ast/class-wp-pdo-statement.php b/wp-includes/sqlite-ast/class-wp-pdo-statement.php index 9893cc4c..9f2d7d06 100644 --- a/wp-includes/sqlite-ast/class-wp-pdo-statement.php +++ b/wp-includes/sqlite-ast/class-wp-pdo-statement.php @@ -30,9 +30,11 @@ public function rowCount(): int { #[ReturnTypeWillChange] public function fetch( int $mode = PDO::FETCH_DEFAULT, int $cursorOrientation = PDO::FETCH_ORI_NEXT, int $cursorOffset = 0 ) { + return $this->stmt->fetch( $mode, $cursorOrientation, $cursorOffset ); } public function fetchAll( int $mode = PDO::FETCH_DEFAULT, ...$args ): array { + return $this->stmt ? $this->stmt->fetchAll( $mode, ...$args ) : array(); } #[ReturnTypeWillChange] @@ -42,6 +44,7 @@ public function fetchColumn( int $column = 0 ) { #[ReturnTypeWillChange] public function fetchObject( ?string $class = 'stdClass', array $constructorArgs = array() ) { + return $this->stmt->fetchObject( $class, $constructorArgs ); } public function getColumnMeta( int $column ): array { diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-connection.php b/wp-includes/sqlite-ast/class-wp-sqlite-connection.php index b015d7c9..8b01d17c 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-connection.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-connection.php @@ -118,6 +118,13 @@ public function query( string $sql, array $params = array() ): PDOStatement { return $stmt; } + public function prepare( string $sql ): PDOStatement { + if ( $this->query_logger ) { + ( $this->query_logger )( $sql, array() ); + } + return $this->pdo->prepare( $sql ); + } + /** * Returns the ID of the last inserted row. * diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index 2c7f5c3a..05976b5e 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -3,6 +3,7 @@ /* * The SQLite driver uses PDO. Enable PDO function calls: * phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO + * phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDOStatement */ /** @@ -18,12 +19,23 @@ class WP_SQLite_Driver { /** @var WP_MySQL_On_SQLite */ private $mysql_on_sqlite_driver; + /** @var array|null */ + private $last_result; + public function __construct( WP_SQLite_Connection $connection, string $database, int $mysql_version = 80038 ) { - $this->mysql_on_sqlite_driver = new WP_MySQL_On_SQLite( $connection, $database, $mysql_version ); + $this->mysql_on_sqlite_driver = new WP_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; } @@ -67,7 +79,10 @@ 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 ); + $this->flush(); + $stmt = $this->mysql_on_sqlite_driver->query( $query, $fetch_mode, ...$fetch_mode_args ); + $this->last_result = $stmt->fetchAll( $fetch_mode ); + return $this->last_result; } public function create_parser( string $query ): WP_MySQL_Parser { @@ -78,7 +93,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; } /** @@ -101,7 +116,7 @@ public function execute_sqlite_query( string $sql, array $params = array() ): PD } public function begin_transaction(): void { - $this->mysql_on_sqlite_driver->begin_transaction(); + $this->mysql_on_sqlite_driver->beginTransaction(); } public function commit(): void { @@ -109,7 +124,11 @@ public function commit(): void { } public function rollback(): void { - $this->mysql_on_sqlite_driver->rollback(); + $this->mysql_on_sqlite_driver->rollBack(); + } + + private function flush(): void { + $this->last_result = null; } /**