From 98113394d9c417ceb530a8a752f15721c2cccd65 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sat, 28 Mar 2026 23:15:06 +0100 Subject: [PATCH 01/25] Tests: Replace some single quotes with doubles --- features/db-search.feature | 48 +++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/features/db-search.feature b/features/db-search.feature index 9856854a..a0217cfd 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. """ 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. """ 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 @@ -1060,7 +1060,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 From 21c36a08bd978bcf0c4c62e0bb16e6cd1589e52f Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sat, 28 Mar 2026 23:15:32 +0100 Subject: [PATCH 02/25] Use `force_env_on_nix_systems` --- src/DB_Command_SQLite.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 342d84ea..d0e98566 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( '/usr/bin/env sqlite3 --version' ), null, null )->run(); $available = 0 === $result->return_code; } From 79093b350143db6b519ed7faf4613a75416b4b19 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Apr 2026 18:12:53 +0200 Subject: [PATCH 03/25] Do not use `/tmp` --- features/db.feature | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/features/db.feature b/features/db.feature index 493b99c1..11190af5 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 @@ -434,7 +434,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 +446,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 From 707c08ab0648810b2dd0b1e07c0533e75a53c283 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Apr 2026 18:13:05 +0200 Subject: [PATCH 04/25] Do not use `/usr/bin/env` --- src/DB_Command_SQLite.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index d0e98566..4fdb8c85 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( Utils\force_env_on_nix_systems( '/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; } From 3f73251d0355053ee256c15d1c8685518c5e7b04 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Apr 2026 18:45:19 +0200 Subject: [PATCH 05/25] Work around missing dbstat table on sqlite --- src/DB_Command.php | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 78cee981..590f78b6 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1210,12 +1210,20 @@ public function size( $args, $assoc_args ) { // Get the table size. if ( $is_sqlite ) { - $table_bytes = $wpdb->get_var( - $wpdb->prepare( - 'SELECT SUM(pgsize) as size_in_bytes FROM dbstat where name = %s LIMIT 1', - $table_name - ) - ); + static $has_dbstat = null; + if ( null === $has_dbstat ) { + $has_dbstat = ! empty( $wpdb->get_results( "SELECT 1 FROM sqlite_master WHERE type='table' AND name='dbstat' LIMIT 1;" ) ); + } + + $table_bytes = 0; + if ( $has_dbstat ) { + $table_bytes = $wpdb->get_var( + $wpdb->prepare( + 'SELECT SUM(pgsize) as size_in_bytes FROM dbstat where name = %s LIMIT 1', + $table_name + ) + ); + } } else { $table_bytes = $wpdb->get_var( $wpdb->prepare( From 8be5f0f456751cbf51922936d693ee5d18de96cd Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Apr 2026 18:46:28 +0200 Subject: [PATCH 06/25] trim & slash --- src/DB_Command_SQLite.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 4fdb8c85..26159ef5 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -320,7 +320,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 ); } @@ -431,6 +432,7 @@ 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' ); + $import_file = str_replace( '\\', '/', $import_file ); file_put_contents( $import_file, $contents ); // Build sqlite3 command. From 655976fceb767aaa0597de03bca9d300c1e9d8f7 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Apr 2026 18:46:37 +0200 Subject: [PATCH 07/25] use different regex delimiter --- features/db-search.feature | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/features/db-search.feature b/features/db-search.feature index a0217cfd..e34340d6 100644 --- a/features/db-search.feature +++ b/features/db-search.feature @@ -766,17 +766,17 @@ Feature: Search through the database """ 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 From 6f3019222d701495d7b684170319be7905cdce22 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Apr 2026 18:47:52 +0200 Subject: [PATCH 08/25] Update features/db-search.feature --- features/db-search.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/db-search.feature b/features/db-search.feature index e34340d6..d54f5d42 100644 --- a/features/db-search.feature +++ b/features/db-search.feature @@ -745,7 +745,7 @@ 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. From 915812a381009a60caf8ffbb512673ae98268c17 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Apr 2026 18:48:01 +0200 Subject: [PATCH 09/25] Update features/db-search.feature --- features/db-search.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/db-search.feature b/features/db-search.feature index d54f5d42..daeaca79 100644 --- a/features/db-search.feature +++ b/features/db-search.feature @@ -734,7 +734,7 @@ Feature: Search through the database 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 From f4514d313667f2110a0cd9c106525e95d910a593 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Apr 2026 21:12:24 +0200 Subject: [PATCH 10/25] revert dbstat change, skip for windows instead --- features/db-size.feature | 2 ++ src/DB_Command.php | 20 ++++++-------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/features/db-size.feature b/features/db-size.feature index b21d9949..72a29344 100644 --- a/features/db-size.feature +++ b/features/db-size.feature @@ -81,6 +81,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 diff --git a/src/DB_Command.php b/src/DB_Command.php index 590f78b6..78cee981 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1210,20 +1210,12 @@ public function size( $args, $assoc_args ) { // Get the table size. if ( $is_sqlite ) { - static $has_dbstat = null; - if ( null === $has_dbstat ) { - $has_dbstat = ! empty( $wpdb->get_results( "SELECT 1 FROM sqlite_master WHERE type='table' AND name='dbstat' LIMIT 1;" ) ); - } - - $table_bytes = 0; - if ( $has_dbstat ) { - $table_bytes = $wpdb->get_var( - $wpdb->prepare( - 'SELECT SUM(pgsize) as size_in_bytes FROM dbstat where name = %s LIMIT 1', - $table_name - ) - ); - } + $table_bytes = $wpdb->get_var( + $wpdb->prepare( + 'SELECT SUM(pgsize) as size_in_bytes FROM dbstat where name = %s LIMIT 1', + $table_name + ) + ); } else { $table_bytes = $wpdb->get_var( $wpdb->prepare( From beef812fdedcd619e36962f0d000df33c3ab2673 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Apr 2026 21:15:05 +0200 Subject: [PATCH 11/25] try to close --- src/DB_Command_SQLite.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 26159ef5..79f91fef 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -149,6 +149,11 @@ protected function sqlite_drop() { WP_CLI::error( 'Database does not exist.' ); } + global $wpdb; + if ( $wpdb instanceof \wpdb ) { + $wpdb->close(); + } + if ( ! unlink( $db_path ) ) { WP_CLI::error( "Could not delete database file: {$db_path}" ); } @@ -168,6 +173,11 @@ protected function sqlite_reset() { // Delete if exists. if ( file_exists( $db_path ) ) { + global $wpdb; + if ( $wpdb instanceof \wpdb ) { + $wpdb->close(); + } + if ( ! unlink( $db_path ) ) { WP_CLI::error( "Could not delete database file: {$db_path}" ); } From ff29cd752f12223d492b1e212f829d505cdb759a Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Apr 2026 21:31:31 +0200 Subject: [PATCH 12/25] slashes --- src/DB_Command_SQLite.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 79f91fef..3961fa24 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -302,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. @@ -412,6 +413,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.' ); From bd0113cd20f8e329bb0ffbcfa29afda5031631d4 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Apr 2026 21:32:57 +0200 Subject: [PATCH 13/25] skip another test --- features/db-search.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/features/db-search.feature b/features/db-search.feature index daeaca79..f5e3225e 100644 --- a/features/db-search.feature +++ b/features/db-search.feature @@ -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 From ac63df0eda525c6580296d0c0ac867ce23dcc58e Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Apr 2026 22:05:05 +0200 Subject: [PATCH 14/25] more skips --- features/db-size.feature | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/features/db-size.feature b/features/db-size.feature index 72a29344..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 @@ -224,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 @@ -284,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 From 025e5608291629efc45bc7d68afe8de0ae53c7d3 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Apr 2026 22:13:01 +0200 Subject: [PATCH 15/25] try more aggressive dropping --- src/DB_Command_SQLite.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 3961fa24..7643ab12 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -153,6 +153,8 @@ protected function sqlite_drop() { if ( $wpdb instanceof \wpdb ) { $wpdb->close(); } + unset( $wpdb ); + gc_collect_cycles(); if ( ! unlink( $db_path ) ) { WP_CLI::error( "Could not delete database file: {$db_path}" ); @@ -177,6 +179,8 @@ protected function sqlite_reset() { if ( $wpdb instanceof \wpdb ) { $wpdb->close(); } + unset( $wpdb ); + gc_collect_cycles(); if ( ! unlink( $db_path ) ) { WP_CLI::error( "Could not delete database file: {$db_path}" ); From eec9530bba13d403895424f2fce20eb0afbfdaef Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Apr 2026 22:13:31 +0200 Subject: [PATCH 16/25] try quoting --- src/DB_Command_SQLite.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 7643ab12..416b606d 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -474,7 +474,7 @@ protected function sqlite_import( $file, $assoc_args ) { ); // Pass the .read dot-command as a single quoted argument (sqlite3 reads it as SQL input). - $command .= ' ' . escapeshellarg( '.read ' . $import_file ); + $command .= ' ' . escapeshellarg( ".read '" . $import_file . "'" ); WP_CLI::debug( "Running shell command: {$command}", 'db' ); From 1caeac9af83294e1cad55b46078f81c5a2e6aa6d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 14 Apr 2026 09:31:43 +0200 Subject: [PATCH 17/25] Try unlink in a loop --- src/DB_Command_SQLite.php | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 416b606d..b6ebd089 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -156,7 +156,22 @@ protected function sqlite_drop() { unset( $wpdb ); gc_collect_cycles(); - if ( ! unlink( $db_path ) ) { + $attempts = 0; + $unlinked = false; + while ( $attempts < 10 ) { + if ( ! file_exists( $db_path ) ) { + $unlinked = true; + break; + } + if ( @unlink( $db_path ) ) { + $unlinked = true; + break; + } + ++$attempts; + usleep( 100000 ); // 100ms + } + + if ( ! $unlinked ) { WP_CLI::error( "Could not delete database file: {$db_path}" ); } @@ -182,7 +197,22 @@ protected function sqlite_reset() { unset( $wpdb ); gc_collect_cycles(); - if ( ! unlink( $db_path ) ) { + $attempts = 0; + $unlinked = false; + while ( $attempts < 10 ) { + if ( ! file_exists( $db_path ) ) { + $unlinked = true; + break; + } + if ( @unlink( $db_path ) ) { + $unlinked = true; + break; + } + ++$attempts; + usleep( 100000 ); // 100ms + } + + if ( ! $unlinked ) { WP_CLI::error( "Could not delete database file: {$db_path}" ); } } From bea936a7f7a529fd5137caa686c22ab61e1f8ab3 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 14 Apr 2026 09:32:03 +0200 Subject: [PATCH 18/25] Combine DROP statements --- src/DB_Command_SQLite.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index b6ebd089..468e5a7d 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -389,9 +389,8 @@ protected function sqlite_export( $file, $assoc_args ) { } 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 ); + $sql = implode( ' ', $drop_statements ); + $command = Utils\esc_cmd( 'sqlite3 %s %s', $temp_db, $sql ); WP_CLI::debug( "Running shell command: {$command}", 'db' ); From acbe7050336bda0e2c44fe38ac571511b644a6ba Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 14 Apr 2026 09:32:10 +0200 Subject: [PATCH 19/25] Avoid nested loops --- src/DB_Command_SQLite.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 468e5a7d..c4b1ef17 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -494,6 +494,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. @@ -502,8 +504,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' ); From c9965c1fd95fd11ce2ea3c96cf31780913802b82 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 14 Apr 2026 10:16:59 +0200 Subject: [PATCH 20/25] Increase attempts --- src/DB_Command_SQLite.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index c4b1ef17..8c300d18 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -158,7 +158,7 @@ protected function sqlite_drop() { $attempts = 0; $unlinked = false; - while ( $attempts < 10 ) { + while ( $attempts < 30 ) { if ( ! file_exists( $db_path ) ) { $unlinked = true; break; @@ -199,7 +199,7 @@ protected function sqlite_reset() { $attempts = 0; $unlinked = false; - while ( $attempts < 10 ) { + while ( $attempts < 30 ) { if ( ! file_exists( $db_path ) ) { $unlinked = true; break; From 9347b95697ac54c26c6a25410b18279ba1b62188 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 14 Apr 2026 10:19:08 +0200 Subject: [PATCH 21/25] Refactor export to use a single sqlite3 process --- src/DB_Command_SQLite.php | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 8c300d18..dd9dda37 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -388,26 +388,28 @@ protected function sqlite_export( $file, $assoc_args ) { $drop_statements[] = sprintf( 'DROP TABLE %s;', $escaped_identifier ); } - if ( ! empty( $drop_statements ) ) { - $sql = implode( ' ', $drop_statements ); - $command = Utils\esc_cmd( 'sqlite3 %s %s', $temp_db, $sql ); - - WP_CLI::debug( "Running shell command: {$command}", 'db' ); + $init_file = tempnam( sys_get_temp_dir(), 'export_init' ); + $init_file = str_replace( '\\', '/', $init_file ); - $result = \WP_CLI\Process::create( $command, null, null )->run(); - - if ( 0 !== $result->return_code ) { - WP_CLI::error( 'Could not export database' ); - } + $init_contents = ''; + if ( ! empty( $drop_statements ) ) { + $init_contents .= implode( "\n", $drop_statements ) . "\n"; } + $init_contents .= ".dump\n"; + + file_put_contents( $init_file, $init_contents ); - $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(); + unlink( $init_file ); if ( 0 !== $result->return_code ) { + if ( file_exists( $temp_db ) ) { + unlink( $temp_db ); + } WP_CLI::error( 'Could not export database' ); } From 4393f9326c5bb01f20ea0fd3d26fb87765d3defe Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 14 Apr 2026 14:37:28 +0200 Subject: [PATCH 22/25] undo changes and skip tests instead --- features/db.feature | 6 ++++-- src/DB_Command_SQLite.php | 34 ++-------------------------------- 2 files changed, 6 insertions(+), 34 deletions(-) diff --git a/features/db.feature b/features/db.feature index 11190af5..7a58c3a2 100644 --- a/features/db.feature +++ b/features/db.feature @@ -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: diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index dd9dda37..997e73c8 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -156,22 +156,7 @@ protected function sqlite_drop() { unset( $wpdb ); gc_collect_cycles(); - $attempts = 0; - $unlinked = false; - while ( $attempts < 30 ) { - if ( ! file_exists( $db_path ) ) { - $unlinked = true; - break; - } - if ( @unlink( $db_path ) ) { - $unlinked = true; - break; - } - ++$attempts; - usleep( 100000 ); // 100ms - } - - if ( ! $unlinked ) { + if ( ! @unlink( $db_path ) ) { WP_CLI::error( "Could not delete database file: {$db_path}" ); } @@ -197,22 +182,7 @@ protected function sqlite_reset() { unset( $wpdb ); gc_collect_cycles(); - $attempts = 0; - $unlinked = false; - while ( $attempts < 30 ) { - if ( ! file_exists( $db_path ) ) { - $unlinked = true; - break; - } - if ( @unlink( $db_path ) ) { - $unlinked = true; - break; - } - ++$attempts; - usleep( 100000 ); // 100ms - } - - if ( ! $unlinked ) { + if ( ! @unlink( $db_path ) ) { WP_CLI::error( "Could not delete database file: {$db_path}" ); } } From 2a3d50506379bd1fcf7ee3b2bfe1bdb259c0204c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 14 Apr 2026 15:53:16 +0200 Subject: [PATCH 23/25] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/DB_Command_SQLite.php | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 997e73c8..b07cbf1f 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -359,6 +359,13 @@ protected function sqlite_export( $file, $assoc_args ) { } $init_file = tempnam( sys_get_temp_dir(), 'export_init' ); + if ( false === $init_file ) { + if ( file_exists( $temp_db ) ) { + unlink( $temp_db ); + } + + WP_CLI::error( 'Failed to create temporary SQLite init file for export.' ); + } $init_file = str_replace( '\\', '/', $init_file ); $init_contents = ''; @@ -367,14 +374,25 @@ protected function sqlite_export( $file, $assoc_args ) { } $init_contents .= ".dump\n"; - file_put_contents( $init_file, $init_contents ); + $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 -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(); - unlink( $init_file ); + if ( file_exists( $init_file ) ) { + unlink( $init_file ); + } if ( 0 !== $result->return_code ) { if ( file_exists( $temp_db ) ) { @@ -449,8 +467,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' ); + if ( false === $import_file ) { + WP_CLI::error( 'Failed to create a temporary file for SQLite import.' ); + } + $import_file = str_replace( '\\', '/', $import_file ); - file_put_contents( $import_file, $contents ); + $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' ]; From 51ae95092163f6e48b2e1e692649c3dc483547a7 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 14 Apr 2026 15:54:02 +0200 Subject: [PATCH 24/25] Cleanup --- src/DB_Command_SQLite.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index b07cbf1f..08bbfc8d 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -153,8 +153,6 @@ protected function sqlite_drop() { if ( $wpdb instanceof \wpdb ) { $wpdb->close(); } - unset( $wpdb ); - gc_collect_cycles(); if ( ! @unlink( $db_path ) ) { WP_CLI::error( "Could not delete database file: {$db_path}" ); @@ -179,8 +177,6 @@ protected function sqlite_reset() { if ( $wpdb instanceof \wpdb ) { $wpdb->close(); } - unset( $wpdb ); - gc_collect_cycles(); if ( ! @unlink( $db_path ) ) { WP_CLI::error( "Could not delete database file: {$db_path}" ); From 984717ee0fe2d44e30a9a962e9f79ed80ab51bfd Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 14 Apr 2026 17:29:28 +0200 Subject: [PATCH 25/25] Lint fix --- src/DB_Command_SQLite.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 08bbfc8d..77893a00 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -467,7 +467,7 @@ protected function sqlite_import( $file, $assoc_args ) { WP_CLI::error( 'Failed to create a temporary file for SQLite import.' ); } - $import_file = str_replace( '\\', '/', $import_file ); + $import_file = str_replace( '\\', '/', $import_file ); $bytes_written = file_put_contents( $import_file, $contents ); if ( false === $bytes_written ) { if ( file_exists( $import_file ) ) {