From 5965c5a1a85a54d6e59e2604486d27db36807257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jake=C5=A1?= Date: Fri, 18 Sep 2026 10:56:53 +0200 Subject: [PATCH 1/3] Align import fixtures with supported driver behavior Test MySQL backslash escapes with the default SQL mode, since the current driver explicitly rejects NO_BACKSLASH_ESCAPES. Keep assertions for every escape case and expect literal backslashes in quoted identifiers. --- features/sqlite-import.feature | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/features/sqlite-import.feature b/features/sqlite-import.feature index ab4975e..9c7ae4b 100644 --- a/features/sqlite-import.feature +++ b/features/sqlite-import.feature @@ -45,10 +45,9 @@ Feature: WP-CLI SQLite Import Command And the SQLite database should contain the imported data @require-sqlite - Scenario: Import a file with escape sequences + Scenario: Import a file with MySQL backslash escape sequences Given a SQL dump file named "test_import.sql" with content: """ - SET sql_mode='NO_BACKSLASH_ESCAPES'; CREATE TABLE test_table (id INTEGER PRIMARY KEY AUTO_INCREMENT, name TEXT); INSERT INTO test_table (name) VALUES ('Test that escaping a backslash \\ works'); INSERT INTO test_table (name) VALUES ('Test that escaping multiple backslashes \\\\\\ works'); @@ -62,11 +61,11 @@ Feature: WP-CLI SQLite Import Command Success: Imported from 'test_import.sql'. """ And the SQLite database should contain a table named "test_table" - And the "test_table" should contain a row with name "Test that escaping a backslash \\ works" - And the "test_table" should contain a row with name "Test that escaping multiple backslashes \\\\\\ works" - And the "test_table" should contain a row with name "Test that escaping a character \a works" - And the "test_table" should contain a row with name "Test that escaping a backslash followed by a character \\a works" - And the "test_table" should contain a row with name "Test that escaping a backslash and a character \\\a works" + And the "test_table" should contain a row with name "Test that escaping a backslash \ works" + And the "test_table" should contain a row with name "Test that escaping multiple backslashes \\\ works" + And the "test_table" should contain a row with name "Test that escaping a character a works" + And the "test_table" should contain a row with name "Test that escaping a backslash followed by a character \a works" + And the "test_table" should contain a row with name "Test that escaping a backslash and a character \a works" @require-sqlite Scenario: Import a file with newlines in strings @@ -150,7 +149,7 @@ Feature: WP-CLI SQLite Import Command Success: Imported from 'test_import.sql'. """ - And the SQLite database should contain a table named "a'strange`identifier\name" + And the SQLite database should contain a table named "a'strange`identifier\\name" @require-sqlite Scenario: Import a file with whitespace and empty lines From 7a3e60a3ef2692e6f2a1777a71f9b4ba31b5e70b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jakes=CC=8C?= Date: Wed, 23 Sep 2026 17:33:07 +0200 Subject: [PATCH 2/3] Align PHP compatibility checks with PHP 7.4 Match the PHPCompatibility target to the existing Composer requirement. The PHP 5.6 target incorrectly rejects supported features such as Throwable and void return types. --- phpcs.xml.dist | 2 +- phpstan/stubs.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 6ddc54d..5a1df5c 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -38,7 +38,7 @@ - + diff --git a/phpstan/stubs.php b/phpstan/stubs.php index 10dc55a..51777ed 100644 --- a/phpstan/stubs.php +++ b/phpstan/stubs.php @@ -8,8 +8,6 @@ // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound -// phpcs:disable PHPCompatibility.FunctionDeclarations.NewReturnTypeDeclarations -// phpcs:disable PHPCompatibility.FunctionDeclarations.NewParamTypeDeclarations class WP_SQLite_Connection { /** From 5824b3638ea044dcc0f78e097d9f1bdcf2a7e49e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jakes=CC=8C?= Date: Wed, 23 Sep 2026 17:33:08 +0200 Subject: [PATCH 3/3] Add support for the new SQLite storage Resolve DB_PATH or db-path.php at runtime, retaining FQDB compatibility for older integration plugin versions. Export and table listing check that the database exists before opening it. Import uses the configured location and initializes managed storage only when no location has been recorded. Keep storage initialization out of the plugin loader and cover the storage behavior with Behat scenarios. https://github.com/WordPress/sqlite-database-integration/pull/502 --- features/bootstrap/SQLiteFeatureContext.php | 61 +++++++- features/sqlite-storage.feature | 164 ++++++++++++++++++++ phpstan/stubs.php | 7 +- src/Import.php | 2 +- src/SQLiteDatabaseIntegrationLoader.php | 4 + src/SQLiteDriverFactory.php | 57 ++++++- 6 files changed, 288 insertions(+), 7 deletions(-) create mode 100644 features/sqlite-storage.feature diff --git a/features/bootstrap/SQLiteFeatureContext.php b/features/bootstrap/SQLiteFeatureContext.php index a83dbc9..7b9598d 100644 --- a/features/bootstrap/SQLiteFeatureContext.php +++ b/features/bootstrap/SQLiteFeatureContext.php @@ -13,6 +13,60 @@ class SQLiteFeatureContext extends WPCLIFeatureContext implements Context { private $db; private $comment_injection_table_name; + /** + * Install WordPress with the integration plugin under test. + * + * @param string $subdir Installation subdirectory. + * @param string $version WordPress version. + */ + public function install_wp( $subdir = '', $version = '' ): void { + if ( 'sqlite' !== getenv( 'WP_CLI_TEST_DBTYPE' ) ) { + parent::install_wp( $subdir, $version ); + return; + } + + // The upstream installation cache copies only the fixed .ht.sqlite path. + // Install SQLite sites directly so managed storage is also preserved. + $subdir = $this->replace_variables( $subdir ); + $this->create_run_dir(); + $this->download_wp( $subdir, $version ); + + $plugin_directory = getenv( 'WP_CLI_TEST_SQLITE_PLUGIN_DIR' ); + if ( $plugin_directory ) { + if ( ! is_file( $plugin_directory . '/db.copy' ) ) { + throw new Exception( 'WP_CLI_TEST_SQLITE_PLUGIN_DIR must point to an integration plugin directory containing db.copy.' ); + } + $run_dir = $this->variables['RUN_DIR'] . '/' . $subdir; + $target = $run_dir . '/wp-content/mu-plugins/sqlite-database-integration'; + self::remove_dir( $target ); + mkdir( $target ); + // Plugin source checkouts link to the shared driver package. + $this->proc( \WP_CLI\Utils\esc_cmd( 'cp -RL %s/. %s', $plugin_directory, $target ) )->run_check(); + file_put_contents( + $run_dir . '/wp-content/db.php', + str_replace( + [ "'{SQLITE_IMPLEMENTATION_FOLDER_PATH}'", '{SQLITE_PLUGIN}', '/plugins/' ], + [ "__DIR__ . '/mu-plugins/sqlite-database-integration'", 'sqlite-database-integration/load.php', '/mu-plugins/' ], + file_get_contents( $target . '/db.copy' ) + ) + ); + } + + $this->create_config( $subdir, "define( 'DISABLE_WP_CRON', true );" ); + $this->proc( + 'wp core install', + [ + 'url' => 'https://example.com', + 'title' => 'WP CLI Site', + 'admin_user' => 'admin', + 'admin_email' => 'admin@example.com', + 'admin_password' => 'password1', + 'skip-email' => true, + ], + $subdir + )->run_check(); + } + /** * @Given /^a SQL dump file named "([^"]*)" with content:$/ */ @@ -71,8 +125,11 @@ public function theFileShouldExist( $filename ) { private function connectToDatabase() { if ( ! $this->db ) { - $run_dir = $this->variables['RUN_DIR']; - $db_file = $run_dir . '/wp-content/database/.ht.sqlite'; + $result = $this->proc( 'wp eval "echo FQDB;"' )->run_check(); + $db_file = trim( $result->stdout ); + if ( ! is_file( $db_file ) ) { + throw new Exception( 'The WordPress SQLite database does not exist.' ); + } $this->db = new PDO( 'sqlite:' . $db_file ); $this->db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); } diff --git a/features/sqlite-storage.feature b/features/sqlite-storage.feature new file mode 100644 index 0000000..cdba3d5 --- /dev/null +++ b/features/sqlite-storage.feature @@ -0,0 +1,164 @@ +@require-sqlite +Feature: SQLite database storage + Commands use the same database as WordPress before its database drop-in loads. + + Background: + Given a WP installation + + Scenario: Use an existing legacy database without migrating storage + Given a SQL dump file named "test_import.sql" with content: + """ + CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO test_table VALUES (1, 'Imported data'); + """ + When I run `wp eval 'echo FQDB;'` + Then save STDOUT as {DATABASE_PATH} + When I run `cp "{DATABASE_PATH}" original.sqlite` + And I run `rm -r wp-content/database` + And I run `mkdir wp-content/database` + And I run `mv original.sqlite wp-content/database/.ht.sqlite` + And I run `wp sqlite export backup.sql` + Then the backup.sql file should contain: + """ + WP CLI Site + """ + When I run `wp sqlite tables` + Then STDOUT should contain: + """ + wp_options + """ + When I run `wp sqlite import test_import.sql` + And I run `wp sqlite export -` + Then STDOUT should contain: + """ + Imported data + """ + And the wp-content/database/.ht.sqlite file should exist + And the wp-content/database/db-path.php file should not exist + + Scenario: Only import initializes fresh default storage + When I run `wp sqlite export backup.sql` + And I run `rm -r wp-content/database` + And I try `wp sqlite export missing.sql` + Then the return code should be 1 + And the missing.sql file should not exist + When I try `wp sqlite tables` + Then the return code should be 1 + And the wp-content/database directory should not exist + When I run `mkdir wp-content/database` + And I run `wp sqlite import backup.sql` + Then STDOUT should contain: + """ + Success: Imported from 'backup.sql'. + """ + When I run `wp sqlite tables` + Then STDOUT should contain: + """ + wp_options + """ + When I run `wp eval 'echo get_option( "blogname" );'` + Then STDOUT should be: + """ + WP CLI Site + """ + + Scenario: Use DB_PATH without changing existing storage + When I run `wp eval 'echo FQDB;'` + Then save STDOUT as {DATABASE_PATH} + When I run `mkdir custom-database` + And I run `cp "{DATABASE_PATH}" 'custom-database/custom;database.sqlite'` + And I run `wp config set DB_PATH "dirname( __FILE__ ) . '/custom-database/custom;database.sqlite'" --raw` + And I run `wp config set FQDB '{RUN_DIR}/unused.sqlite'` + And I run `wp sqlite export backup.sql` + And I run `wp sqlite import backup.sql` + And I run `wp sqlite tables` + Then STDOUT should contain: + """ + wp_options + """ + And the unused.sqlite file should not exist + And the custom-database/db-path.php file should not exist + And the custom-database/.htaccess file should not exist + And the custom-database/.ht.sqlite.lock file should not exist + When I run `wp sqlite export -` + Then STDOUT should contain: + """ + WP CLI Site + """ + + Scenario: Only import creates a missing explicit database + Given a SQL dump file named "test_import.sql" with content: + """ + CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO test_table VALUES (1, 'Imported data'); + """ + When I run `wp config set DB_PATH '{RUN_DIR}/new.sqlite'` + And I try `wp sqlite export backup.sql` + Then the return code should be 1 + And STDERR should contain: + """ + The SQLite database does not exist. + """ + And the new.sqlite file should not exist + And the backup.sql file should not exist + When I try `wp sqlite tables` + Then the return code should be 1 + And the new.sqlite file should not exist + When I run `wp sqlite import test_import.sql` + And I run `wp sqlite tables` + Then STDOUT should contain: + """ + test_table + """ + When I run `wp sqlite export -` + Then STDOUT should contain: + """ + Imported data + """ + + Scenario: Use the legacy FQDB override + When I run `wp eval 'echo FQDB;'` + Then save STDOUT as {DB_PATH} + When I run `cp "{DB_PATH}" custom.sqlite` + And I run `wp config set FQDB '{RUN_DIR}/custom.sqlite'` + And I run `wp sqlite export backup.sql` + And I run `wp sqlite import backup.sql` + And I run `wp sqlite tables` + Then STDOUT should contain: + """ + wp_options + """ + When I run `wp eval 'echo FQDB;'` + Then STDOUT should be: + """ + {RUN_DIR}/custom.sqlite + """ + When I run `wp eval 'echo get_option( "blogname" );'` + Then STDOUT should be: + """ + WP CLI Site + """ + + Scenario: Reject an invalid DB_PATH without falling back to existing storage + Given a SQL dump file named "test_import.sql" with content: + """ + CREATE TABLE test_table (id INTEGER PRIMARY KEY); + """ + When I run `wp config set DB_PATH null --raw` + And I try `wp sqlite import test_import.sql` + Then the return code should be 1 + And STDERR should contain: + """ + The SQLite database path is invalid. + """ + When I try `wp sqlite export backup.sql` + Then the return code should be 1 + And the backup.sql file should not exist + When I try `wp sqlite tables` + Then the return code should be 1 + When I run `wp config delete DB_PATH` + And I run `wp sqlite tables` + Then STDOUT should not contain: + """ + test_table + """ diff --git a/phpstan/stubs.php b/phpstan/stubs.php index 51777ed..d0d5113 100644 --- a/phpstan/stubs.php +++ b/phpstan/stubs.php @@ -30,7 +30,12 @@ public function get_connection(): WP_SQLite_Connection {} } class WP_SQLite_Translator { - public function __construct() {} + public function __construct( ?PDO $pdo = null ) {} public function query( string $sql ) {} public function get_pdo(): PDO {} } + +class WP_SQLite_Storage { + public function __construct( ?string $database_root = null, ?string $database_path = null ) {} + public function initialize(): string {} +} diff --git a/src/Import.php b/src/Import.php index e312611..a4fa387 100644 --- a/src/Import.php +++ b/src/Import.php @@ -20,7 +20,7 @@ class Import { public function __construct() { SQLiteDatabaseIntegrationLoader::load_plugin(); - $this->driver = SQLiteDriverFactory::create_driver(); + $this->driver = SQLiteDriverFactory::create_driver( true ); } /** diff --git a/src/SQLiteDatabaseIntegrationLoader.php b/src/SQLiteDatabaseIntegrationLoader.php index f235a64..d8cbda4 100644 --- a/src/SQLiteDatabaseIntegrationLoader.php +++ b/src/SQLiteDatabaseIntegrationLoader.php @@ -87,6 +87,10 @@ public static function load_plugin() { require_once $plugin_directory . '/php-polyfills.php'; } require_once $plugin_directory . '/constants.php'; + $storage_file = $plugin_directory . '/wp-includes/sqlite/class-wp-sqlite-storage.php'; + if ( file_exists( $storage_file ) ) { + require_once $storage_file; + } $mysql_on_sqlite_class_file = $plugin_directory . '/wp-includes/database/sqlite/class-wp-mysql-on-sqlite.php'; $mysql_on_sqlite_loader_file = $plugin_directory . '/wp-includes/database/load.php'; diff --git a/src/SQLiteDriverFactory.php b/src/SQLiteDriverFactory.php index acb8129..4d1e6bc 100644 --- a/src/SQLiteDriverFactory.php +++ b/src/SQLiteDriverFactory.php @@ -3,24 +3,38 @@ namespace Automattic\WP_CLI\SQLite; use PDO; +use RuntimeException; +use WP_CLI; use WP_MySQL_On_SQLite; use WP_SQLite_Connection; use WP_SQLite_Driver; +use WP_SQLite_Storage; use WP_SQLite_Translator; class SQLiteDriverFactory { /** * Create an instance of the SQLite driver. * + * @param bool $create_database_if_missing Allow imports to create a database when missing. + * * @return WP_MySQL_On_SQLite|WP_SQLite_Driver|WP_SQLite_Translator */ - public static function create_driver() { + public static function create_driver( $create_database_if_missing = false ) { + try { + $database_path = self::get_database_path( $create_database_if_missing ); + } catch ( \Throwable $exception ) { + WP_CLI::error( 'Could not open the SQLite database: ' . $exception->getMessage() ); + } + if ( ! $create_database_if_missing && ! is_file( $database_path ) ) { + WP_CLI::error( 'The SQLite database does not exist.' ); + } + $db_name = defined( 'DB_NAME' ) && '' !== DB_NAME ? DB_NAME : 'database_name_here'; if ( class_exists( WP_MySQL_On_SQLite::class ) ) { $database_dsn = sprintf( 'mysql-on-sqlite:path=%s;dbname=%s', - str_replace( ';', ';;', FQDB ), + str_replace( ';', ';;', $database_path ), str_replace( ';', ';;', $db_name ) ); $driver = new WP_MySQL_On_SQLite( $database_dsn ); @@ -35,9 +49,46 @@ public static function create_driver() { $connection = new WP_SQLite_Connection( array( - 'path' => FQDB, + 'path' => $database_path, ) ); return new WP_SQLite_Driver( $connection, $db_name ); } + + /** + * Resolve configuration without initializing existing storage. + * + * @param bool $create_database_if_missing Allow initialization when no managed path is recorded. + * @return string + */ + private static function get_database_path( $create_database_if_missing ) { + if ( defined( 'DB_PATH' ) ) { + $database_path = DB_PATH; + } elseif ( defined( 'FQDB' ) ) { + // Older plugin releases and explicit legacy settings use FQDB. + $database_path = FQDB; + } else { + $path_file = rtrim( FQDBDIR, '/\\' ) . '/db-path.php'; + $legacy_path = rtrim( FQDBDIR, '/\\' ) . '/.ht.sqlite'; + if ( file_exists( $path_file ) ) { + // Use include so unreadable files can be handled on PHP 7. + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Failure is checked below. + $database_path = @include $path_file; + if ( false === $database_path ) { + throw new RuntimeException( 'Failed to read the SQLite database path file.' ); + } + } elseif ( is_file( $legacy_path ) ) { + $database_path = $legacy_path; + } elseif ( $create_database_if_missing && class_exists( WP_SQLite_Storage::class ) ) { + $database_path = ( new WP_SQLite_Storage( FQDBDIR ) )->initialize(); + } else { + throw new RuntimeException( 'No SQLite database path is configured.' ); + } + } + + if ( ! is_string( $database_path ) || '' === $database_path || false !== strpos( $database_path, "\0" ) ) { + throw new RuntimeException( 'The SQLite database path is invalid.' ); + } + return $database_path; + } }