diff --git a/features/bootstrap/SQLiteFeatureContext.php b/features/bootstrap/SQLiteFeatureContext.php
index a83dbc9..d543dfa 100644
--- a/features/bootstrap/SQLiteFeatureContext.php
+++ b/features/bootstrap/SQLiteFeatureContext.php
@@ -71,8 +71,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 defined( "DB_PATH" ) ? DB_PATH : 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-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
diff --git a/features/sqlite-storage.feature b/features/sqlite-storage.feature
new file mode 100644
index 0000000..02d6bf5
--- /dev/null
+++ b/features/sqlite-storage.feature
@@ -0,0 +1,195 @@
+@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 defined( "DB_PATH" ) ? DB_PATH : 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 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 try `wp sqlite import missing.sql`
+ Then the return code should be 1
+ And STDERR should contain:
+ """
+ Unable to open file: missing.sql
+ """
+ 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 defined( "DB_PATH" ) ? DB_PATH : 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 sqlite export backup.sql`
+ And I run `wp sqlite import backup.sql`
+ Then 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 try `wp sqlite import missing.sql`
+ Then the return code should be 1
+ And STDERR should contain:
+ """
+ Unable to open file: missing.sql
+ """
+ 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 defined( "DB_PATH" ) ? DB_PATH : 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 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:
+ """
+ Error: Could not open the SQLite database:
+ """
+ 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
+ """
+
+ Scenario: Reject an in-memory database
+ 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 ':memory:'`
+ And I try `wp sqlite import test_import.sql`
+ Then the return code should be 1
+ And STDERR should contain:
+ """
+ In-memory databases are not supported.
+ """
+ When I try `wp sqlite export backup.sql`
+ Then the return code should be 1
+ And STDERR should contain:
+ """
+ In-memory databases are not supported.
+ """
+ And the backup.sql file should not exist
+ When I try `wp sqlite tables`
+ Then the return code should be 1
+ And STDERR should contain:
+ """
+ In-memory databases are not supported.
+ """
+
+ Scenario: Report a database that cannot be opened
+ 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 '{RUN_DIR}/missing-directory/database.sqlite'`
+ And I try `wp sqlite import test_import.sql`
+ Then the return code should be 1
+ And STDERR should contain:
+ """
+ Error: Could not open the SQLite database:
+ """
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..7d5d730 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 {
/**
@@ -36,3 +34,9 @@ public function __construct() {}
public function query( string $sql ) {}
public function get_pdo(): PDO {}
}
+
+class WP_SQLite_Storage {
+ public static function with_secret_path( string $root ): self {}
+ public function initialize(): string {}
+ private function __construct() {}
+}
diff --git a/src/Import.php b/src/Import.php
index e312611..aaf6286 100644
--- a/src/Import.php
+++ b/src/Import.php
@@ -20,7 +20,6 @@ class Import {
public function __construct() {
SQLiteDatabaseIntegrationLoader::load_plugin();
- $this->driver = SQLiteDriverFactory::create_driver();
}
/**
@@ -36,6 +35,13 @@ public function run( $sql_file_path, $args ) {
$is_stdin = '-' === $sql_file_path;
$import_file = $is_stdin ? 'php://stdin' : $sql_file_path;
+ $handle = fopen( $import_file, 'r' );
+ if ( ! $handle ) {
+ WP_CLI::error( "Unable to open file: $import_file" );
+ }
+
+ // Create the driver after opening the input, so a missing file doesn't create a database.
+ $this->driver = SQLiteDriverFactory::create_driver( true );
/*
* Set default SQL mode and other options as per the "mysqldump" command.
@@ -61,7 +67,8 @@ public function run( $sql_file_path, $args ) {
*/
$this->driver->execute_sqlite_query( 'PRAGMA foreign_keys = OFF' );
- $this->execute_statements( $import_file );
+ $this->execute_statements( $handle );
+ fclose( $handle );
/*
* Re-enable foreign key constraints and verify integrity.
@@ -83,13 +90,13 @@ public function run( $sql_file_path, $args ) {
/**
* Execute SQL statements from an SQL dump file.
*
- * @param $import_file
+ * @param resource $handle The SQL dump file handle.
*
* @return void
* @throws Exception
*/
- protected function execute_statements( $import_file ) {
- foreach ( $this->parse_statements( $import_file ) as $statement ) {
+ protected function execute_statements( $handle ) {
+ foreach ( $this->parse_statements( $handle ) as $statement ) {
try {
$this->driver->query( $statement );
} catch ( Exception $e ) {
@@ -114,18 +121,11 @@ protected function execute_statements( $import_file ) {
/**
* Parse SQL statements from an SQL dump file.
- * @param string $sql_file_path The path to the SQL dump file.
+ * @param resource $handle The SQL dump file handle.
*
* @return Generator A generator that yields SQL statements.
*/
- public function parse_statements( $sql_file_path ) {
-
- $handle = fopen( $sql_file_path, 'r' );
-
- if ( ! $handle ) {
- WP_CLI::error( "Unable to open file: $sql_file_path" );
- }
-
+ public function parse_statements( $handle ) {
$starting_quote = null;
$in_comment = false;
$buffer = '';
@@ -202,7 +202,5 @@ public function parse_statements( $sql_file_path ) {
if ( ! empty( $buffer ) ) {
yield $buffer;
}
-
- fclose( $handle );
}
}
diff --git a/src/SQLiteDatabaseIntegrationLoader.php b/src/SQLiteDatabaseIntegrationLoader.php
index f235a64..a639302 100644
--- a/src/SQLiteDatabaseIntegrationLoader.php
+++ b/src/SQLiteDatabaseIntegrationLoader.php
@@ -86,7 +86,16 @@ public static function load_plugin() {
if ( $old_structure ) {
require_once $plugin_directory . '/php-polyfills.php';
}
- require_once $plugin_directory . '/constants.php';
+ try {
+ require_once $plugin_directory . '/constants.php';
+ } catch ( \Throwable $exception ) {
+ // Newer plugin releases reject invalid database settings.
+ WP_CLI::error( 'Could not open the SQLite database: ' . $exception->getMessage() );
+ }
+ $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..7e8de01 100644
--- a/src/SQLiteDriverFactory.php
+++ b/src/SQLiteDriverFactory.php
@@ -3,24 +3,93 @@
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( $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.' );
+ }
+
+ try {
+ $driver = self::open_driver( $database_path );
+ } catch ( \Throwable $exception ) {
+ WP_CLI::error( 'Could not open the SQLite database: ' . $exception->getMessage() );
+ }
+ return $driver;
+ }
+
+ /**
+ * Resolve configuration without initializing existing storage.
+ *
+ * @param bool $create_database_if_missing Allow initialization when no secret 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 = WP_SQLite_Storage::with_secret_path( 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.' );
+ }
+ if ( ':memory:' === $database_path ) {
+ throw new RuntimeException( 'In-memory databases are not supported.' );
+ }
+ return $database_path;
+ }
+
+ /**
+ * Open the SQLite driver for a database path.
+ *
+ * @param string $database_path The SQLite database path.
* @return WP_MySQL_On_SQLite|WP_SQLite_Driver|WP_SQLite_Translator
*/
- public static function create_driver() {
+ private static function open_driver( $database_path ) {
$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,7 +104,7 @@ public static function create_driver() {
$connection = new WP_SQLite_Connection(
array(
- 'path' => FQDB,
+ 'path' => $database_path,
)
);
return new WP_SQLite_Driver( $connection, $db_name );