Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,21 @@ $ wp sqlite import <file>
<file>
The path to the MySQL compatible dump file to import. When passing `-` as the file argument, the SQL commands are read from standard input.

[--refuse-overwrite]
Refuse to import when the SQLite database already contains user tables. By default the import merges into the existing database.

[--verify-old-host=<host>]
After loading, fail when any stored value still contains <host>, or when any PHP-serialized value no longer unserializes. The check runs inside the import transaction, so a failure rolls everything back. A failed import into a new database file also deletes that file and its `-wal`, `-shm`, and `-journal` sidecars.

[--enable-ast-driver]
Enables the AST driver for older integration plugin releases. The current driver API is selected automatically.

### wp sqlite export

Exports an SQLite database to a MySQL compatible file.

Each `INSERT` statement is written on a single line: embedded newlines and other special bytes are backslash-escaped (including double quotes), so dumps are safe for line-oriented downstream rewriters as well as for quote-aware parsers.

```
$ wp sqlite export [<file>] [--tables=<tables>] [--exclude-tables] [--porcelain]
```
Expand Down
90 changes: 89 additions & 1 deletion features/bootstrap/SQLiteFeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ public function theSqliteDatabaseShouldContainATableNamed( $table_name ) {
}
}

/**
* @Then /^the SQLite database should not contain a table named "([^"]*)"$/
*/
public function theSqliteDatabaseShouldNotContainATableNamed( $table_name ) {
$this->connectToDatabase();
$stmt = $this->db->prepare( "SELECT name FROM sqlite_master WHERE type='table' AND name=?" );
$stmt->execute( [ $table_name ] );
$row = $stmt->fetch();
if ( $row ) {
throw new Exception( "Table '$table_name' was found in the database but should not exist." );
}
}
/**
* @Then /^the "([^"]*)" should contain a row with name "([^"]*)"$/
* @Then /^the "([^"]*)" should contain a row with name:$/
Expand Down Expand Up @@ -71,8 +83,15 @@ public function theFileShouldExist( $filename ) {

private function connectToDatabase() {
if ( ! $this->db ) {
$result = $this->proc( 'wp eval \'echo defined( "DB_PATH" ) ? DB_PATH : FQDB;\'' )->run_check();
// Prefer the configured path without booting WordPress: the
// target may be a fresh import without WP tables installed.
$result = $this->proc( 'wp config get DB_PATH' )->run();
$db_file = trim( $result->stdout );
if ( '' === $db_file || 0 === strpos( $db_file, 'Error:' ) ) {
// Fall back to a full boot so the drop-in resolves FQDB.
$result = $this->proc( 'wp eval \'echo defined( "FQDB" ) ? FQDB : ""; \'' )->run_check();
$db_file = trim( $result->stdout );
}
if ( ! is_file( $db_file ) ) {
throw new Exception( 'The WordPress SQLite database does not exist.' );
}
Expand Down Expand Up @@ -345,4 +364,73 @@ public function iExportTheTableWithACommentInjectionName() {
'run'
);
}

/**
* @Given /^the SQLite database contains an option with a double quote$/
*/
public function theSqliteDatabaseContainsAnOptionWithADoubleQuote() {
$this->connectToDatabase();

$this->db->exec( "DELETE FROM wp_options WHERE option_name = 'double-quote-option'" );

$stmt = $this->db->prepare(
'
INSERT INTO wp_options (option_name, option_value, autoload)
VALUES (:option_name, :option_value, :autoload)
'
);
$stmt->execute(
array(
':option_name' => 'double-quote-option',
':option_value' => 'say "hello" backslash\\ end',
':autoload' => 'yes',
)
);
}

/**
* @Then /^the double quote option should be escaped and preserved$/
*/
public function theDoubleQuoteOptionShouldBeEscapedAndPreserved() {
$this->connectToDatabase();

$stmt = $this->db->prepare( "SELECT option_value FROM wp_options WHERE option_name = 'double-quote-option'" );
$stmt->execute();
$value = $stmt->fetchColumn();

$expected = 'say "hello" backslash\\ end';
if ( $expected !== $value ) {
throw new Exception(
sprintf(
'Double quote option was not preserved. Expected %s, got %s.',
var_export( $expected, true ),
var_export( $value, true )
)
);
}
}

/**
* @Then /^every INSERT in the file "([^"]*)" should be on a single line$/
*/
public function everyInsertInTheFileShouldBeOnASingleLine( $filename ) {
$full_path = $this->variables['RUN_DIR'] . '/' . $filename;
$lines = file( $full_path, FILE_IGNORE_NEW_LINES );
if ( false === $lines ) {
throw new Exception( "Could not read file: $full_path" );
}
$insert_count = 0;
foreach ( $lines as $line ) {
if ( 0 !== strpos( ltrim( $line ), 'INSERT INTO' ) ) {
continue;
}
++$insert_count;
if ( ';' !== substr( rtrim( $line ), -1 ) ) {
throw new Exception( "INSERT statement spans multiple lines: $line" );
}
}
if ( 0 === $insert_count ) {
throw new Exception( "No INSERT statements found in file: $full_path" );
}
}
}
29 changes: 28 additions & 1 deletion features/sqlite-export.feature
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ Feature: WP-CLI SQLite Export Command
And the file "test_export_serialized_settings.sql" should exist
And the file "test_export_serialized_settings.sql" should contain:
"""
breadcrumb-separator";s:4:"\00bb
breadcrumb-separator\";s:4:\"\00bb
"""
And the file "test_export_serialized_settings.sql" should contain:
"""
Expand Down Expand Up @@ -201,3 +201,30 @@ Feature: WP-CLI SQLite Export Command
Success: Imported from 'test_export_escape_bytes.sql'.
"""
And the SQL string escape bytes should be preserved

@require-sqlite
Scenario: Export should escape double quotes with a backslash
Given the SQLite database contains an option with a double quote
When I run `wp sqlite export test_export_dquote.sql --tables=wp_options`
Then STDOUT should contain:
"""
Success: Export complete. File written to test_export_dquote.sql
"""
And the file "test_export_dquote.sql" should exist
And the file "test_export_dquote.sql" should contain:
"""
'double-quote-option','say \"hello\" backslash\\ end','yes'
"""
When I run `wp sqlite --enable-ast-driver import test_export_dquote.sql`
Then STDOUT should contain:
"""
Success: Imported from 'test_export_dquote.sql'.
"""
And the double quote option should be escaped and preserved

@require-sqlite
Scenario: Export keeps every INSERT on a single line
Given the SQLite database contains an option with SQL string escape bytes
When I run `wp sqlite export test_export_single_line.sql --tables=wp_options`
Then the file "test_export_single_line.sql" should exist
And every INSERT in the file "test_export_single_line.sql" should be on a single line
99 changes: 99 additions & 0 deletions features/sqlite-import.feature
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,102 @@ Feature: WP-CLI SQLite Import Command
"""
And the SQLite database should contain a table named "wp_zz_parent"
And the SQLite database should contain a table named "wp_aa_child"

@require-sqlite
Scenario: Refuse to overwrite an existing 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 (name) VALUES ('Test Name');
"""
When I run `wp sqlite import test_import.sql`
Then STDOUT should contain:
"""
Success: Imported from 'test_import.sql'.
"""
When I try `wp sqlite import test_import.sql --refuse-overwrite`
Then the return code should be 1
And STDERR should contain:
"""
already contains tables
"""
And the SQLite database should contain a table named "test_table"

@require-sqlite
Scenario: Allow refuse-overwrite into a fresh database file
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 (name) VALUES ('Test Name');
"""
When I run `wp config set DB_PATH '{RUN_DIR}/fresh.sqlite'`
And I run `wp sqlite import test_import.sql --refuse-overwrite`
Then STDOUT should contain:
"""
Success: Imported from 'test_import.sql'.
"""
And the SQLite database should contain a table named "test_table"

@require-sqlite
Scenario: Failed import into a new database removes the new file
Given a SQL dump file named "test_import_bad.sql" with content:
"""
CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO missing_table (name) VALUES ('Test Name');
"""
When I run `wp config set DB_PATH '{RUN_DIR}/atomic.sqlite'`
And I try `wp sqlite import test_import_bad.sql`
Then the return code should be 1
And STDERR should contain:
"""
Could not execute statement
"""
And the atomic.sqlite file should not exist
And the atomic.sqlite-wal file should not exist
And the atomic.sqlite-shm file should not exist
And the atomic.sqlite-journal file should not exist

@require-sqlite
Scenario: Fail verification when the old host remains
Given a SQL dump file named "test_import.sql" with content:
"""
CREATE TABLE verify_host (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO verify_host (name) VALUES ('https://oldhost.invalid/page');
"""
When I try `wp sqlite import test_import.sql --verify-old-host=oldhost.invalid`
Then the return code should be 1
And STDERR should contain:
"""
still contain 'oldhost.invalid'
"""
And the SQLite database should not contain a table named "verify_host"

@require-sqlite
Scenario: Pass verification when the old host is gone
Given a SQL dump file named "test_import.sql" with content:
"""
CREATE TABLE verify_host (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO verify_host (name) VALUES ('https://newhost.invalid/page');
"""
When I run `wp sqlite import test_import.sql --verify-old-host=oldhost.invalid`
Then STDOUT should contain:
"""
Success: Imported from 'test_import.sql'.
"""
And the SQLite database should contain a table named "verify_host"
And the "verify_host" should contain a row with name "https://newhost.invalid/page"

@require-sqlite
Scenario: Fail verification on corrupt serialized data
Given a SQL dump file named "test_import.sql" with content:
"""
CREATE TABLE verify_serialized (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO verify_serialized (name) VALUES ('a:1:{s:3:"foo";s:4:"broken}');
"""
When I try `wp sqlite import test_import.sql --verify-old-host=never-present.invalid`
Then the return code should be 1
And STDERR should contain:
"""
invalid serialized data
"""
And the SQLite database should not contain a table named "verify_serialized"
11 changes: 11 additions & 0 deletions src/Export.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ protected function get_create_statement( $table_name ) {
/**
* Get the INSERT statements for a table.
*
* Each yielded statement is a single line: embedded newlines and other
* special bytes are backslash-escaped by escape_string(), and the caller
* terminates every statement with PHP_EOL. Line-oriented downstream
* rewriters may therefore process the dump one statement per line.
*
* @param string $table_name
*
* @return \Generator
Expand Down Expand Up @@ -269,6 +274,11 @@ protected function escape_values( $values ) {
/**
* Escapes a string for use in an insert statement.
*
* Uses full MySQL backslash escaping, including double quotes, so that
* each INSERT statement stays on a single line and remains safe for
* line-oriented downstream rewriters (e.g. go-search-replace) as well
* as for the quote-aware Import::parse_statements().
*
* @param $value
*
* @return string
Expand All @@ -281,6 +291,7 @@ protected function escape_string( $value ) {
"\n" => '\n',
"\r" => '\r',
"\x1a" => '\Z',
'"' => '\\"',
'\\' => '\\\\',
"'" => "\\'",
)
Expand Down
Loading