Skip to content
5 changes: 3 additions & 2 deletions features/bootstrap/SQLiteFeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function aSqlDumpFileNamedWithContent( $filename, PyStringNode $content )
*/
public function theSqliteDatabaseShouldContainATableNamed( $table_name ) {
$this->connectToDatabase();
$result = $this->db->query( "SELECT name FROM sqlite_master WHERE type='table' AND name='$table_name'" );
$result = $this->db->query( "SELECT name FROM sqlite_master WHERE type='table' AND name='" . $this->db->escapeString( $table_name ) . "'" );
$row = $result->fetchArray();
if ( ! $row ) {
throw new Exception( "Table '$table_name' not found in the database." );
Expand All @@ -34,10 +34,11 @@ public function theSqliteDatabaseShouldContainATableNamed( $table_name ) {

/**
* @Then /^the "([^"]*)" should contain a row with name "([^"]*)"$/
* @Then /^the "([^"]*)" should contain a row with name:$/
*/
public function theTableShouldContainARowWithName( $table_name, $name ) {
$this->connectToDatabase();
$result = $this->db->query( "SELECT * FROM $table_name WHERE name='$name'" );
$result = $this->db->query( "SELECT * FROM $table_name WHERE name='" . $this->db->escapeString( $name ) . "'" );
$row = $result->fetchArray();
if ( ! $row ) {
throw new Exception( "Row with name '$name' not found in table '$table_name'." );
Expand Down
128 changes: 128 additions & 0 deletions features/sqlite-import.feature
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,131 @@ Feature: WP-CLI SQLite Import Command
Success: Imported from 'STDIN'.
"""
And the SQLite database should contain the imported data

@require-sqlite
Scenario: Import a file with 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');
INSERT INTO test_table (name) VALUES ('Test that escaping a character \a works');
INSERT INTO test_table (name) VALUES ('Test that escaping a backslash followed by a character \\a works');
INSERT INTO test_table (name) VALUES ('Test that escaping a backslash and a character \\\a works');
"""
When I run `wp sqlite --enable-ast-driver import test_import.sql`
Then STDOUT should contain:
"""
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"

@require-sqlite
Scenario: Import a file with newlines in strings
Given a SQL dump file named "test_import.sql" with content:
"""
CREATE TABLE test_table (id INTEGER PRIMARY KEY AUTO_INCREMENT, name TEXT);
INSERT INTO test_table (name) VALUES ('Test that a string containing
a newline character and some whitespace works');
"""
When I run `wp sqlite --enable-ast-driver import test_import.sql`
Then STDOUT should contain:
"""
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 a string containing
a newline character and some whitespace works
"""

@require-sqlite
Scenario: Import a file with comments
Given a SQL dump file named "test_import.sql" with content:
"""
CREATE TABLE test_table (id INTEGER PRIMARY KEY AUTO_INCREMENT, name TEXT);
-- This is an inline comment.
# This is an inline comment.
INSERT INTO test_table (name) VALUES ('one'); -- This is an inline comment.
/* This is a block comment */
INSERT INTO test_table (name) VALUES ('two'); /* This
is a block comment
on multiple lines */ INSERT INTO test_table (name) VALUES ('three');
INSERT INTO test_table (name) VALUES ('fo -- this looks like a comment ur');
INSERT INTO test_table (name) VALUES ('fi/* this looks like a comment */ve');
"""
When I run `wp sqlite --enable-ast-driver import test_import.sql`
Then STDOUT should contain:
"""
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 "one"
And the "test_table" should contain a row with name "two"
And the "test_table" should contain a row with name "three"
And the "test_table" should contain a row with name "fo -- this looks like a comment ur"
And the "test_table" should contain a row with name "fi/* this looks like a comment */ve"

@require-sqlite
Scenario: Import a file quoted strings
Given a SQL dump file named "test_import.sql" with content:
"""
CREATE TABLE test_table (id INTEGER PRIMARY KEY AUTO_INCREMENT, name TEXT);
INSERT INTO test_table (name) VALUES ('a single-quoted string with \' '' some " tricky ` chars');
INSERT INTO test_table (name) VALUES ("a double-quoted string with ' some \" "" tricky ` chars");
"""
When I run `wp sqlite --enable-ast-driver import test_import.sql`
Then STDOUT should contain:
"""
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:
"""
a single-quoted string with ' ' some " tricky ` chars
"""
And the "test_table" should contain a row with name:
"""
a double-quoted string with ' some " " tricky ` chars
"""

@require-sqlite
Scenario: Import a file backtick-quoted identifiers
Given a SQL dump file named "test_import.sql" with content:
"""
CREATE TABLE `a'strange``identifier\\name` (id INTEGER PRIMARY KEY AUTO_INCREMENT, name TEXT);
"""
When I run `wp sqlite --enable-ast-driver import test_import.sql`
Then STDOUT should contain:
"""
Success: Imported from 'test_import.sql'.
"""

And the SQLite database should contain a table named "a'strange`identifier\name"

@require-sqlite
Scenario: Import a file with whitespace and empty lines
Given a SQL dump file named "test_import.sql" with content:
"""

CREATE TABLE test_table (id INTEGER PRIMARY KEY AUTO_INCREMENT, name TEXT);


INSERT INTO test_table (name) VALUES ('Test Name');

"""
When I run `wp sqlite --enable-ast-driver import test_import.sql`
Then STDOUT should contain:
"""
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 Name"
89 changes: 57 additions & 32 deletions src/Import.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,52 +73,76 @@ public function parse_statements( $sql_file_path ) {
WP_CLI::error( "Unable to open file: $sql_file_path" );
}

$single_quotes = 0;
$double_quotes = 0;
$in_comment = false;
$buffer = '';
$starting_quote = null;
$in_comment = false;
$buffer = '';

// phpcs:ignore
while ( ( $line = fgets( $handle ) ) !== false ) {
$line = trim( $line );

// Skip empty lines and comments
if ( empty( $line ) || strpos( $line, '--' ) === 0 || strpos( $line, '#' ) === 0 ) {
continue;
}

// Handle multi-line comments
if ( ! $in_comment && strpos( $line, '/*' ) === 0 ) {
$in_comment = true;
}
if ( $in_comment ) {
if ( strpos( $line, '*/' ) !== false ) {
$in_comment = false;
}
continue;
// Detect and convert encoding to UTF-8
$detected_encoding = mb_detect_encoding( $line, mb_list_encodings(), true );
if ( $detected_encoding && 'UTF-8' !== $detected_encoding ) {
$line = mb_convert_encoding( $line, 'UTF-8', $detected_encoding );
}

$strlen = strlen( $line );
for ( $i = 0; $i < $strlen; $i++ ) {
$ch = $line[ $i ];

// Handle escaped characters
if ( $i > 0 && '\\' === $line[ $i - 1 ] ) {
$buffer .= $ch;
continue;
// Handle escape sequences in single and double quoted strings.
// TODO: Support NO_BACKSLASH_ESCAPES SQL mode.
if ( "'" === $ch || '"' === $ch ) {
// Count preceding backslashes.
$slashes = 0;
while ( $slashes < $i && '\\' === $line[ $i - $slashes - 1 ] ) {
++$slashes;
}

// Handle escaped characters.
// A characters is escaped only when the number of preceding backslashes
// is odd - "\" is an escape sequence, "\\" is an escaped backslash.
if ( 1 === $slashes % 2 ) {
$buffer .= $ch;
continue;
}
}

// Handle quotes
if ( "'" === $ch && 0 === $double_quotes ) {
$single_quotes = 1 - $single_quotes;
// Handle comments.
if ( null === $starting_quote ) {
$prev_ch = isset( $line[ $i - 1 ] ) ? $line[ $i - 1 ] : null;
$next_ch = isset( $line[ $i + 1 ] ) ? $line[ $i + 1 ] : null;

// Skip inline comments.
if ( ( '-' === $ch && '-' === $next_ch ) || '#' === $ch ) {
break; // Stop for the current line.
}

// Skip multi-line comments.
if ( ! $in_comment && '/' === $ch && '*' === $next_ch ) {
$in_comment = true;
continue;
}
if ( $in_comment ) {
if ( '*' === $prev_ch && '/' === $ch ) {
$in_comment = false;
}
continue;
}
}
if ( '"' === $ch && 0 === $single_quotes ) {
$double_quotes = 1 - $double_quotes;

// Handle quotes
if ( null === $starting_quote && ( "'" === $ch || '"' === $ch || '`' === $ch ) ) {
$starting_quote = $ch;
} elseif ( null !== $starting_quote && $ch === $starting_quote ) {
$starting_quote = null;
}

// Process statement end
if ( ';' === $ch && 0 === $single_quotes && 0 === $double_quotes ) {
yield trim( $buffer );
if ( ';' === $ch && null === $starting_quote ) {
$buffer = trim( $buffer );
if ( ! empty( $buffer ) ) {
yield $buffer;
}
$buffer = '';
} else {
$buffer .= $ch;
Expand All @@ -127,8 +151,9 @@ public function parse_statements( $sql_file_path ) {
}

// Handle any remaining buffer content
$buffer = trim( $buffer );
if ( ! empty( $buffer ) ) {
yield trim( $buffer );
yield $buffer;
}

fclose( $handle );
Expand Down