diff --git a/features/bootstrap/SQLiteFeatureContext.php b/features/bootstrap/SQLiteFeatureContext.php index 87598ca..9a07975 100644 --- a/features/bootstrap/SQLiteFeatureContext.php +++ b/features/bootstrap/SQLiteFeatureContext.php @@ -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." ); @@ -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'." ); diff --git a/features/sqlite-import.feature b/features/sqlite-import.feature index 547d1d0..75a4365 100644 --- a/features/sqlite-import.feature +++ b/features/sqlite-import.feature @@ -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" diff --git a/src/Import.php b/src/Import.php index d9aa644..de13bb0 100644 --- a/src/Import.php +++ b/src/Import.php @@ -51,10 +51,21 @@ public function run( $sql_file_path, $args ) { */ protected function execute_statements( $import_file ) { foreach ( $this->parse_statements( $import_file ) as $statement ) { - $result = $this->driver->query( $statement ); - if ( false === $result ) { - WP_CLI::warning( 'Could not execute statement: ' . $statement ); - echo $this->driver->get_error_message(); + try { + $this->driver->query( $statement ); + } catch ( Exception $e ) { + try { + // Try converting encoding and retry + $detected_encoding = mb_detect_encoding( $statement, mb_list_encodings(), true ); + if ( $detected_encoding && 'UTF-8' !== $detected_encoding ) { + $converted_statement = mb_convert_encoding( $statement, 'UTF-8', $detected_encoding ); + echo 'Converted ecoding for statement: ' . $converted_statement . PHP_EOL; + $this->driver->query( $converted_statement ); + } + } catch ( Exception $e ) { + WP_CLI::warning( 'Could not execute statement: ' . $statement ); + echo $e->getMessage(); + } } } } @@ -73,52 +84,70 @@ 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; - } - $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; @@ -127,8 +156,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 );