diff --git a/features/db-search.feature b/features/db-search.feature index 9856854a..f5e3225e 100644 --- a/features/db-search.feature +++ b/features/db-search.feature @@ -679,19 +679,19 @@ Feature: Search through the database :あいうえおかきくけこさしすせとたちつてと """ - When I run `wp db search 'ppppp' --before_context=3 --after_context=4` + When I run `wp db search "ppppp" --before_context=3 --after_context=4` Then STDOUT should contain: """ :ムnöppppp """ - When I run `wp db search 'ppppp' --before_context=1 --after_context=1` + When I run `wp db search "ppppp" --before_context=1 --after_context=1` Then STDOUT should contain: """ :öppppp """ - When I run `wp db search 'ムn' --before_context=2 --after_context=1` + When I run `wp db search "ムn" --before_context=2 --after_context=1` Then STDOUT should contain: """ :llムnö @@ -701,7 +701,7 @@ Feature: Search through the database :llムnöp """ - When I run `wp db search 'ムn' --before_context=2 --after_context=2` + When I run `wp db search "ムn" --before_context=2 --after_context=2` Then STDOUT should contain: """ :llムnöp @@ -720,7 +720,7 @@ Feature: Search through the database # Note ö is o with combining umlaut. And I run `wp option update regextst_combining 'lllllムnöppppp'` - When I run `wp db search 'https?:\/\/example.c.m' --regex` + When I run `wp db search "https?:\/\/example.c.m" --regex` Then STDOUT should contain: """ wp_options:option_value @@ -731,10 +731,10 @@ Feature: Search through the database [...] """ - When I run `wp db search 'unfindable' --regex` + When I run `wp db search "unfindable" --regex` Then STDOUT should be empty - When I try `wp db search 'unfindable' --regex --regex-flags='abcd'` + When I try `wp db search "unfindable" --regex --regex-flags="abcd"` Then STDERR should contain: """ unfindable @@ -745,42 +745,42 @@ Feature: Search through the database """ And the return code should be 1 - When I try `wp db search 'unfindable' --regex --regex-delimiter='1'` + When I try `wp db search "unfindable" --regex --regex-delimiter="1"` Then STDERR should be: """ Error: The regex '1unfindable1' fails. """ And the return code should be 1 - When I try `wp db search 'regex error)' --regex` + When I try `wp db search "regex error)" --regex` Then STDERR should be: """ Error: The regex pattern 'regex error)' with default delimiter 'chr(1)' and no flags fails. """ And the return code should be 1 - When I try `wp db search 'regex error)' --regex --regex-flags=u` + When I try `wp db search "regex error)" --regex --regex-flags=u` Then STDERR should be: """ Error: The regex pattern 'regex error)' with default delimiter 'chr(1)' and flags 'u' fails. """ And the return code should be 1 - When I try `wp db search 'regex error)' --regex --regex-delimiter=/` + When I try `wp db search "regex error)" --regex --regex-delimiter=#` Then STDERR should be: """ - Error: The regex '/regex error)/' fails. + Error: The regex '#regex error)#' fails. """ And the return code should be 1 - When I try `wp db search 'regex error)' --regex --regex-delimiter=/ --regex-flags=u` + When I try `wp db search "regex error)" --regex --regex-delimiter=# --regex-flags=u` Then STDERR should be: """ - Error: The regex '/regex error)/u' fails. + Error: The regex '#regex error)#u' fails. """ And the return code should be 1 - When I run `wp db search '[0-9é]+?https:' --regex --regex-flags=u --before_context=0 --after_context=0` + When I run `wp db search "[0-9é]+?https:" --regex --regex-flags=u --before_context=0 --after_context=0` Then STDOUT should contain: """ :1234567890123456789éhttps: @@ -794,7 +794,7 @@ Feature: Search through the database [...] """ - When I run `wp db search 'htt(p(s):)\/\/' --regex --before_context=1 --after_context=3` + When I run `wp db search "htt(p(s):)\/\/" --regex --before_context=1 --after_context=3` Then STDOUT should contain: """ :あhttps://reg [...] éhttps://reg @@ -804,7 +804,7 @@ Feature: Search through the database rege """ - When I run `wp db search 'https://' --regex --regex-delimiter=# --before_context=9 --after_context=11` + When I run `wp db search "https://" --regex --regex-delimiter=# --before_context=9 --after_context=11` Then STDOUT should contain: """ :2345é789あhttps://regextst.co [...] 23456789éhttps://regextst.co @@ -814,10 +814,10 @@ Feature: Search through the database regextst.com """ - When I run `wp db search 'httPs://' --regex --regex-delimiter=# --before_context=3 --after_context=0` + When I run `wp db search "httPs://" --regex --regex-delimiter=# --before_context=3 --after_context=0` Then STDOUT should be empty - When I run `wp db search 'httPs://' --regex --regex-flags=i --regex-delimiter=# --before_context=3 --after_context=0` + When I run `wp db search "httPs://" --regex --regex-flags=i --regex-delimiter=# --before_context=3 --after_context=0` Then STDOUT should contain: """ :89あhttps:// [...] 89éhttps:// @@ -827,19 +827,19 @@ Feature: Search through the database https://r """ - When I run `wp db search 'ppppp' --regex --before_context=3 --after_context=4` + When I run `wp db search "ppppp" --regex --before_context=3 --after_context=4` Then STDOUT should contain: """ :ムnöppppp """ - When I run `wp db search 'ppppp' --regex --before_context=1 --after_context=1` + When I run `wp db search "ppppp" --regex --before_context=1 --after_context=1` Then STDOUT should contain: """ :öppppp """ - When I run `wp db search 'ムn' --before_context=2 --after_context=1` + When I run `wp db search "ムn" --before_context=2 --after_context=1` Then STDOUT should contain: """ :llムnö @@ -849,7 +849,7 @@ Feature: Search through the database :llムnöp """ - When I run `wp db search 'ムn' --regex --before_context=2 --after_context=2` + When I run `wp db search "ムn" --regex --before_context=2 --after_context=2` Then STDOUT should contain: """ :llムnöp @@ -859,7 +859,7 @@ Feature: Search through the database :llムnöpp """ - When I run `wp db search 't\.c' --regex --before_context=1 --after_context=1` + When I run `wp db search "t\.c" --regex --before_context=1 --after_context=1` Then STDOUT should contain: """ :st.co [...] st.co [...] st.co [...] 0t.co @@ -869,7 +869,7 @@ Feature: Search through the database st.com """ - When I run `wp db search 'https://' --regex` + When I run `wp db search "https://" --regex` Then the return code should be 0 Scenario: Search with output options @@ -982,6 +982,7 @@ Feature: Search through the database And STDOUT should match /\d tables? skipped:.*wp_term_relationships/ And STDERR should be empty + @skip-windows Scenario: Search with custom colors Given a WP install @@ -1060,7 +1061,7 @@ Feature: Search through the database When I run `wp db query "SOURCE esc_sql_ident.sql;"` Then STDERR should be empty - When I run `wp db search 'v_v' TABLE --all-tables` + When I run `wp db search "v_v" TABLE --all-tables` Then STDOUT should be: """ TABLE:VALUES diff --git a/features/db-size.feature b/features/db-size.feature index b21d9949..17f0d687 100644 --- a/features/db-size.feature +++ b/features/db-size.feature @@ -32,6 +32,8 @@ Feature: Display database size B """ + # On CI, SQLite on Windows is missing the dbstat extension. + @skip-windows Scenario: Display only table sizes for a WordPress install Given a WP install @@ -81,6 +83,8 @@ Feature: Display database size KB """ + # On CI, SQLite on Windows is missing the dbstat extension. + @skip-windows Scenario: Display only table sizes in a human readable format for a WordPress install Given a WP install @@ -222,6 +226,8 @@ Feature: Display database size But STDOUT should not be a number + # On CI, SQLite on Windows is missing the dbstat extension. + @skip-windows Scenario: Display all table sizes for a WordPress install Given a WP install @@ -282,6 +288,8 @@ Feature: Display database size [{"Name":"wp_posts", """ + # On CI, SQLite on Windows is missing the dbstat extension. + @skip-windows Scenario: Display ordered table sizes for a WordPress install Given a WP install diff --git a/features/db.feature b/features/db.feature index 493b99c1..7a58c3a2 100644 --- a/features/db.feature +++ b/features/db.feature @@ -256,7 +256,7 @@ Feature: Perform database operations 1 """ - When I run `wp db export /tmp/wp-cli-behat.sql` + When I run `wp db export wp-cli-behat.sql` Then STDOUT should contain: """ Success: Exported @@ -283,7 +283,7 @@ Feature: Perform database operations When I try `wp post list --format=count` Then STDERR should not be empty - When I run `wp db import /tmp/wp-cli-behat.sql` + When I run `wp db import wp-cli-behat.sql` Then STDOUT should contain: """ Success: Imported @@ -312,7 +312,7 @@ Feature: Perform database operations When I run `wp db create` Then STDOUT should not be empty - When I run `wp db export /tmp/wp-cli-behat.sql` + When I run `wp db export wp-cli-behat.sql` Then STDOUT should contain: """ Success: Exported @@ -383,7 +383,8 @@ Feature: Perform database operations Query succeeded. Rows affected: 1 """ - @require-sqlite + @require-sqlite @skip-windows + # Skipped on Windows due to persistent file locking issues when run via Behat. Scenario: SQLite DB CRUD operations Given a WP install And a session_yes file: @@ -420,7 +421,8 @@ Feature: Perform database operations total """ - @require-sqlite + @require-sqlite @skip-windows + # Skipped on Windows due to persistent file locking issues when run via Behat. Scenario: SQLite DB export/import Given a WP install And a session_yes file: @@ -434,7 +436,7 @@ Feature: Perform database operations 1 """ - When I run `wp db export /tmp/wp-cli-sqlite-behat.sql` + When I run `wp db export wp-cli-sqlite-behat.sql` Then STDOUT should contain: """ Success: Exported @@ -446,7 +448,7 @@ Feature: Perform database operations Success: Database reset """ - When I run `wp db import /tmp/wp-cli-sqlite-behat.sql` + When I run `wp db import wp-cli-sqlite-behat.sql` Then STDOUT should contain: """ Success: Imported diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 342d84ea..77893a00 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -20,7 +20,7 @@ protected function is_sqlite3_available() { static $available = null; if ( null === $available ) { - $result = \WP_CLI\Process::create( '/usr/bin/env sqlite3 --version', null, null )->run(); + $result = \WP_CLI\Process::create( Utils\force_env_on_nix_systems( 'sqlite3' ) . ' --version', null, null )->run(); $available = 0 === $result->return_code; } @@ -149,7 +149,12 @@ protected function sqlite_drop() { WP_CLI::error( 'Database does not exist.' ); } - if ( ! unlink( $db_path ) ) { + global $wpdb; + if ( $wpdb instanceof \wpdb ) { + $wpdb->close(); + } + + if ( ! @unlink( $db_path ) ) { WP_CLI::error( "Could not delete database file: {$db_path}" ); } @@ -168,7 +173,12 @@ protected function sqlite_reset() { // Delete if exists. if ( file_exists( $db_path ) ) { - if ( ! unlink( $db_path ) ) { + global $wpdb; + if ( $wpdb instanceof \wpdb ) { + $wpdb->close(); + } + + if ( ! @unlink( $db_path ) ) { WP_CLI::error( "Could not delete database file: {$db_path}" ); } } @@ -292,6 +302,7 @@ protected function sqlite_export( $file, $assoc_args ) { if ( false === $temp_db ) { WP_CLI::error( 'Could not create temporary database file for export.' ); } + $temp_db = str_replace( '\\', '/', $temp_db ); if ( ! copy( $db_path, $temp_db ) ) { // Clean up temporary file if the copy operation fails. @@ -320,7 +331,8 @@ protected function sqlite_export( $file, $assoc_args ) { WP_CLI::error( 'Could not export database' ); } - $all_tables = explode( "\n", $result->stdout ); + $all_tables = array_map( 'trim', explode( "\n", $result->stdout ) ); + $all_tables = array_filter( $all_tables ); $exclude_tables = array_diff( $all_tables, $include_tables ); } @@ -342,27 +354,46 @@ protected function sqlite_export( $file, $assoc_args ) { $drop_statements[] = sprintf( 'DROP TABLE %s;', $escaped_identifier ); } - if ( ! empty( $drop_statements ) ) { - $args = array_merge( array( 'sqlite3', $temp_db ), $drop_statements ); - $placeholders = array_fill( 0, count( $args ), '%s' ); - $command = Utils\esc_cmd( implode( ' ', $placeholders ), ...$args ); + $init_file = tempnam( sys_get_temp_dir(), 'export_init' ); + if ( false === $init_file ) { + if ( file_exists( $temp_db ) ) { + unlink( $temp_db ); + } - WP_CLI::debug( "Running shell command: {$command}", 'db' ); + WP_CLI::error( 'Failed to create temporary SQLite init file for export.' ); + } + $init_file = str_replace( '\\', '/', $init_file ); - $result = \WP_CLI\Process::create( $command, null, null )->run(); + $init_contents = ''; + if ( ! empty( $drop_statements ) ) { + $init_contents .= implode( "\n", $drop_statements ) . "\n"; + } + $init_contents .= ".dump\n"; - if ( 0 !== $result->return_code ) { - WP_CLI::error( 'Could not export database' ); + $bytes_written = file_put_contents( $init_file, $init_contents ); + if ( false === $bytes_written ) { + if ( file_exists( $init_file ) ) { + unlink( $init_file ); } + if ( file_exists( $temp_db ) ) { + unlink( $temp_db ); + } + WP_CLI::error( 'Could not export database' ); } - $command = Utils\esc_cmd( 'sqlite3 %s .dump', $temp_db ); + $command = Utils\esc_cmd( 'sqlite3 -init %s %s .exit', $init_file, $temp_db ); WP_CLI::debug( "Running shell command: {$command}", 'db' ); $result = \WP_CLI\Process::create( $command, null, null )->run(); + if ( file_exists( $init_file ) ) { + unlink( $init_file ); + } if ( 0 !== $result->return_code ) { + if ( file_exists( $temp_db ) ) { + unlink( $temp_db ); + } WP_CLI::error( 'Could not export database' ); } @@ -401,6 +432,7 @@ protected function sqlite_import( $file, $assoc_args ) { if ( ! $db_path ) { WP_CLI::error( 'Could not determine the database path.' ); } + $db_path = str_replace( '\\', '/', $db_path ); if ( ! file_exists( $db_path ) ) { WP_CLI::error( 'Database does not exist.' ); @@ -431,7 +463,18 @@ protected function sqlite_import( $file, $assoc_args ) { $contents = preg_replace( '/\bCREATE UNIQUE INDEX (?!IF NOT EXISTS\b)/i', 'CREATE UNIQUE INDEX IF NOT EXISTS ', (string) $contents ); $import_file = tempnam( sys_get_temp_dir(), 'temp.db' ); - file_put_contents( $import_file, $contents ); + if ( false === $import_file ) { + WP_CLI::error( 'Failed to create a temporary file for SQLite import.' ); + } + + $import_file = str_replace( '\\', '/', $import_file ); + $bytes_written = file_put_contents( $import_file, $contents ); + if ( false === $bytes_written ) { + if ( file_exists( $import_file ) ) { + unlink( $import_file ); + } + WP_CLI::error( sprintf( 'Failed to write SQLite import data to temporary file: %s', $import_file ) ); + } // Build sqlite3 command. $command_parts = [ 'sqlite3' ]; @@ -447,6 +490,8 @@ protected function sqlite_import( $file, $assoc_args ) { $command_parts[] = 'PRAGMA journal_mode=MEMORY;'; } + $command_parts[] = '-init'; + $command_parts[] = $import_file; $command_parts[] = $db_path; // Build a properly escaped string command. Process::create() requires a string, not an array. @@ -455,8 +500,8 @@ protected function sqlite_import( $file, $assoc_args ) { ...$command_parts ); - // Pass the .read dot-command as a single quoted argument (sqlite3 reads it as SQL input). - $command .= ' ' . escapeshellarg( '.read ' . $import_file ); + // Pass .exit to quit interactive mode after running -init. + $command .= ' ' . escapeshellarg( '.exit' ); WP_CLI::debug( "Running shell command: {$command}", 'db' );