From 5c54007569bb455c4e08caa88af5375cb050fce8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:41:03 +0000 Subject: [PATCH 01/31] Initial plan From cfe43e7d2d878bce44aa3ba64a3417e24bddee80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:56:25 +0000 Subject: [PATCH 02/31] Add wpdb fallback for wp db query when mysql/mariadb binary is unavailable Agent-Logs-Url: https://github.com/wp-cli/db-command/sessions/8996fc65-2840-4792-8cf4-ddc700a7c040 Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/db-query.feature | 26 ++++++++ src/DB_Command.php | 133 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/features/db-query.feature b/features/db-query.feature index 48a50af5..5d2a865f 100644 --- a/features/db-query.feature +++ b/features/db-query.feature @@ -119,3 +119,29 @@ Feature: Query the database with WordPress' MySQL config """ ANSI """ + + @require-mysql-or-mariadb + Scenario: Database querying falls back to wpdb when mysql binary is unavailable + Given a WP install + And a fake-bin/mysql file: + """ + #!/bin/sh + exit 127 + """ + And a fake-bin/mariadb file: + """ + #!/bin/sh + exit 127 + """ + + When I run `chmod +x fake-bin/mysql fake-bin/mariadb` + And I run `env PATH={RUN_DIR}/fake-bin:$PATH wp db query "SELECT COUNT(ID) FROM wp_users;" --debug` + Then STDOUT should be: + """ + COUNT(ID) + 1 + """ + And STDERR should contain: + """ + MySQL/MariaDB binary not available, falling back to wpdb. + """ diff --git a/src/DB_Command.php b/src/DB_Command.php index e0d7ad47..cc75a177 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -591,6 +591,25 @@ public function query( $args, $assoc_args ) { return; } + if ( ! $this->is_mysql_binary_available() ) { + // Get the query from args or STDIN. + $query = ''; + if ( ! empty( $args ) ) { + $query = $args[0]; + } else { + $query = stream_get_contents( STDIN ); + } + + if ( empty( $query ) ) { + WP_CLI::error( 'No query specified.' ); + } + + WP_CLI::debug( 'MySQL/MariaDB binary not available, falling back to wpdb.', 'db' ); + $this->maybe_load_wpdb(); + $this->wpdb_query( $query, $assoc_args ); + return; + } + $command = sprintf( '/usr/bin/env %s%s --no-auto-rehash', $this->get_mysql_command(), @@ -2348,4 +2367,118 @@ protected function get_current_sql_modes( $assoc_args ) { private function get_mysql_command() { return 'mariadb' === Utils\get_db_type() ? 'mariadb' : 'mysql'; } + + /** + * Check if the mysql or mariadb binary is available. + * + * @return bool True if the binary is available, false otherwise. + */ + protected function is_mysql_binary_available() { + static $available = null; + + if ( null === $available ) { + $binary = $this->get_mysql_command(); + $result = \WP_CLI\Process::create( "/usr/bin/env {$binary} --version", null, null )->run(); + $available = 0 === $result->return_code; + } + + return $available; + } + + /** + * Load WordPress's wpdb if not already available. + * + * Loads the minimal required WordPress files to make $wpdb available, + * including any db.php drop-in (e.g., HyperDB or other custom drivers). + */ + protected function maybe_load_wpdb() { + global $wpdb; + + if ( isset( $wpdb ) ) { + return; + } + + if ( ! defined( 'WPINC' ) ) { + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound + define( 'WPINC', 'wp-includes' ); + } + + if ( ! defined( 'WP_CONTENT_DIR' ) ) { + define( 'WP_CONTENT_DIR', ABSPATH . 'wp-content' ); + } + + // Load required WordPress files if not already loaded. + if ( ! function_exists( 'add_action' ) ) { + $required_files = [ + ABSPATH . WPINC . '/compat.php', + ABSPATH . WPINC . '/plugin.php', + // Defines `wp_debug_backtrace_summary()` as used by wpdb. + ABSPATH . WPINC . '/functions.php', + ABSPATH . WPINC . '/class-wpdb.php', + ]; + + foreach ( $required_files as $required_file ) { + if ( file_exists( $required_file ) ) { + require_once $required_file; + } + } + } + + // Load db.php drop-in if it exists (e.g., HyperDB or other custom drivers). + $db_dropin_path = WP_CONTENT_DIR . '/db.php'; + if ( file_exists( $db_dropin_path ) && ! $this->is_sqlite() ) { + require_once $db_dropin_path; + } + + // If $wpdb is still not set (e.g. no drop-in), create a new instance using the DB credentials from wp-config.php. + if ( ! isset( $GLOBALS['wpdb'] ) && class_exists( 'wpdb' ) ) { + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $wpdb = new wpdb( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST ); + } + } + + /** + * Execute a query against the database using wpdb. + * + * Used as a fallback when the mysql/mariadb binary is not available. + * + * @param string $query SQL query to execute. + * @param array $assoc_args Associative arguments. + */ + protected function wpdb_query( $query, $assoc_args = [] ) { + global $wpdb; + + if ( ! isset( $wpdb ) || ! ( $wpdb instanceof wpdb ) ) { + WP_CLI::error( 'WordPress database (wpdb) is not available. Please install MySQL or MariaDB client tools.' ); + } + + $skip_column_names = Utils\get_flag_value( $assoc_args, 'skip-column-names', false ); + + $is_row_modifying_query = preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE(?!\s*\()|LOAD DATA)\b/i', $query ); + + if ( $is_row_modifying_query ) { + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $affected_rows = $wpdb->query( $query ); + if ( false === $affected_rows ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags + WP_CLI::error( 'Query failed: ' . strip_tags( $wpdb->last_error ) ); + } + WP_CLI::success( "Query succeeded. Rows affected: {$affected_rows}" ); + } else { + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $results = $wpdb->get_results( $query, ARRAY_A ); + + if ( $wpdb->last_error ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags + WP_CLI::error( 'Query failed: ' . strip_tags( $wpdb->last_error ) ); + } + + if ( empty( $results ) ) { + return; + } + + $headers = array_keys( $results[0] ); + $this->display_query_results( $headers, $results, $skip_column_names ); + } + } } From ea5dea17f25731ab1709ddc8c55ed41016efdb98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 21:30:49 +0000 Subject: [PATCH 03/31] Improve wpdb fallback: add escapeshellarg and table prefix support Agent-Logs-Url: https://github.com/wp-cli/db-command/sessions/8996fc65-2840-4792-8cf4-ddc700a7c040 Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/DB_Command.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index cc75a177..62eacfab 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -2378,7 +2378,7 @@ protected function is_mysql_binary_available() { if ( null === $available ) { $binary = $this->get_mysql_command(); - $result = \WP_CLI\Process::create( "/usr/bin/env {$binary} --version", null, null )->run(); + $result = \WP_CLI\Process::create( '/usr/bin/env ' . escapeshellarg( $binary ) . ' --version', null, null )->run(); $available = 0 === $result->return_code; } @@ -2434,6 +2434,9 @@ protected function maybe_load_wpdb() { if ( ! isset( $GLOBALS['wpdb'] ) && class_exists( 'wpdb' ) ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $wpdb = new wpdb( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST ); + if ( isset( $GLOBALS['table_prefix'] ) && is_string( $GLOBALS['table_prefix'] ) ) { + $wpdb->set_prefix( $GLOBALS['table_prefix'] ); + } } } From 5375be87f7c3003547e66193914ff8bd48c7a872 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 07:39:23 +0000 Subject: [PATCH 04/31] Add load.php, extend wpdb fallback to run_query() and import command Agent-Logs-Url: https://github.com/wp-cli/db-command/sessions/2fe9988f-095f-4c61-b162-5bf4f8956cca Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/DB_Command.php | 164 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/src/DB_Command.php b/src/DB_Command.php index 62eacfab..44fbc84c 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -929,6 +929,30 @@ public function import( $args, $assoc_args ) { return; } + if ( ! $this->is_mysql_binary_available() ) { + if ( '-' === $result_file ) { + $sql_content = stream_get_contents( STDIN ); + if ( false === $sql_content ) { + WP_CLI::error( 'Failed to read from STDIN.' ); + } + $result_file = 'STDIN'; + } else { + if ( ! is_readable( $result_file ) ) { + WP_CLI::error( sprintf( 'Import file missing or not readable: %s', $result_file ) ); + } + $sql_content = file_get_contents( $result_file ); + if ( false === $sql_content ) { + WP_CLI::error( sprintf( 'Could not read import file: %s', $result_file ) ); + } + } + + WP_CLI::debug( 'MySQL/MariaDB binary not available, falling back to wpdb for import.', 'db' ); + $this->maybe_load_wpdb(); + $this->wpdb_import( (string) $sql_content, $assoc_args ); + WP_CLI::success( sprintf( "Imported from '%s'.", $result_file ) ); + return; + } + // Process options to MySQL. $mysql_args = array_merge( [ 'database' => DB_NAME ], @@ -1919,6 +1943,19 @@ private static function get_create_query() { * @param array $assoc_args Optional. Associative array of arguments. */ protected function run_query( $query, $assoc_args = [] ) { + if ( ! $this->is_mysql_binary_available() ) { + WP_CLI::debug( "Query via wpdb: {$query}", 'db' ); + $this->maybe_load_wpdb(); + global $wpdb; + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $result = $wpdb->query( $query ); + if ( false === $result ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags + WP_CLI::error( 'Query failed: ' . strip_tags( $wpdb->last_error ) ); + } + return; + } + // Ensure that the SQL mode is compatible with WPDB. $query = $this->get_sql_mode_query( $assoc_args ) . $query; @@ -2410,6 +2447,7 @@ protected function maybe_load_wpdb() { // Load required WordPress files if not already loaded. if ( ! function_exists( 'add_action' ) ) { $required_files = [ + ABSPATH . WPINC . '/load.php', ABSPATH . WPINC . '/compat.php', ABSPATH . WPINC . '/plugin.php', // Defines `wp_debug_backtrace_summary()` as used by wpdb. @@ -2484,4 +2522,130 @@ protected function wpdb_query( $query, $assoc_args = [] ) { $this->display_query_results( $headers, $results, $skip_column_names ); } } + + /** + * Import SQL content into the database using wpdb. + * + * Used as a fallback when the mysql/mariadb binary is not available. + * + * @param string $sql_content SQL content to import. + * @param array $assoc_args Associative arguments. + */ + protected function wpdb_import( $sql_content, $assoc_args = [] ) { + global $wpdb; + + if ( ! isset( $wpdb ) || ! ( $wpdb instanceof wpdb ) ) { + WP_CLI::error( 'WordPress database (wpdb) is not available. Please install MySQL or MariaDB client tools.' ); + } + + $skip_optimization = Utils\get_flag_value( $assoc_args, 'skip-optimization', false ); + + if ( ! $skip_optimization ) { + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'SET autocommit = 0' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'SET unique_checks = 0' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'SET foreign_key_checks = 0' ); + } + + $statements = $this->split_sql_statements( $sql_content ); + + foreach ( $statements as $statement ) { + $statement = trim( $statement ); + if ( '' === $statement ) { + continue; + } + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $result = $wpdb->query( $statement ); + if ( false === $result ) { + if ( ! $skip_optimization ) { + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'ROLLBACK' ); + } + // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags + WP_CLI::error( 'Import failed: ' . strip_tags( $wpdb->last_error ) ); + } + } + + if ( ! $skip_optimization ) { + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'COMMIT' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'SET autocommit = 1' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'SET unique_checks = 1' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'SET foreign_key_checks = 1' ); + } + } + + /** + * Split a SQL string into individual statements. + * + * Handles single-quoted strings, double-quoted strings, and comments + * so that semicolons inside them are not treated as statement delimiters. + * + * @param string $sql SQL string to split. + * @return string[] Array of individual SQL statements. + */ + private function split_sql_statements( $sql ) { + $statements = []; + $current = ''; + $in_single_quote = false; + $in_double_quote = false; + $in_comment = false; + $in_line_comment = false; + $length = strlen( $sql ); + + for ( $i = 0; $i < $length; $i++ ) { + $char = $sql[ $i ]; + $next = ( $i + 1 < $length ) ? $sql[ $i + 1 ] : ''; + + if ( $in_line_comment ) { + if ( "\n" === $char ) { + $in_line_comment = false; + } + continue; + } + + if ( $in_comment ) { + if ( '*' === $char && '/' === $next ) { + $in_comment = false; + ++$i; + } + continue; + } + + if ( '/' === $char && '*' === $next && ! $in_single_quote && ! $in_double_quote ) { + $in_comment = true; + ++$i; + continue; + } + + if ( '-' === $char && '-' === $next && ! $in_single_quote && ! $in_double_quote ) { + $in_line_comment = true; + continue; + } + + if ( "'" === $char && ! $in_double_quote ) { + $in_single_quote = ! $in_single_quote; + } elseif ( '"' === $char && ! $in_single_quote ) { + $in_double_quote = ! $in_double_quote; + } + + if ( ';' === $char && ! $in_single_quote && ! $in_double_quote ) { + $statements[] = $current; + $current = ''; + } else { + $current .= $char; + } + } + + if ( '' !== trim( $current ) ) { + $statements[] = $current; + } + + return $statements; + } } From 47906b99e75f53ac983651f98e19951bc40a90ed Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Apr 2026 11:02:25 +0200 Subject: [PATCH 05/31] Apply suggestion from @swissspidy --- features/db-query.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/db-query.feature b/features/db-query.feature index 5d2a865f..4e736563 100644 --- a/features/db-query.feature +++ b/features/db-query.feature @@ -135,7 +135,7 @@ Feature: Query the database with WordPress' MySQL config """ When I run `chmod +x fake-bin/mysql fake-bin/mariadb` - And I run `env PATH={RUN_DIR}/fake-bin:$PATH wp db query "SELECT COUNT(ID) FROM wp_users;" --debug` + And I try `env PATH={RUN_DIR}/fake-bin:$PATH wp db query "SELECT COUNT(ID) FROM wp_users;" --debug` Then STDOUT should be: """ COUNT(ID) From 2d3706f3bd9f7c8f109e34157b86f9326141ab3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 09:11:15 +0000 Subject: [PATCH 06/31] Add Behat tests for wpdb fallback in drop, reset, and import commands Agent-Logs-Url: https://github.com/wp-cli/db-command/sessions/fa41acc5-de2a-4dc3-9f7a-f5b282ed670a Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/db-import.feature | 28 +++++++++++++++++++++ features/db.feature | 50 ++++++++++++++++++++++++++++++++++++++ src/DB_Command.php | 5 ++++ 3 files changed, 83 insertions(+) diff --git a/features/db-import.feature b/features/db-import.feature index 8438d06b..0af8c19d 100644 --- a/features/db-import.feature +++ b/features/db-import.feature @@ -69,6 +69,34 @@ Feature: Import a WordPress database Success: Imported from 'wp_cli_test.sql'. """ + @require-mysql-or-mariadb + Scenario: Database import falls back to wpdb when mysql binary is unavailable + Given a WP install + And a fake-bin/mysql file: + """ + #!/bin/sh + exit 127 + """ + And a fake-bin/mariadb file: + """ + #!/bin/sh + exit 127 + """ + + When I run `wp db export wp_cli_test.sql` + Then the wp_cli_test.sql file should exist + + When I run `chmod +x fake-bin/mysql fake-bin/mariadb` + And I run `env PATH={RUN_DIR}/fake-bin:$PATH wp db import wp_cli_test.sql --debug` + Then STDOUT should be: + """ + Success: Imported from 'wp_cli_test.sql'. + """ + And STDERR should contain: + """ + MySQL/MariaDB binary not available, falling back to wpdb for import. + """ + # SQLite doesn't support the --dbuser flag. @require-mysql-or-mariadb Scenario: Import from database name path by default with passed-in dbuser/dbpass diff --git a/features/db.feature b/features/db.feature index 493b99c1..51207006 100644 --- a/features/db.feature +++ b/features/db.feature @@ -383,6 +383,56 @@ Feature: Perform database operations Query succeeded. Rows affected: 1 """ + @require-mysql-or-mariadb + Scenario: Database drop falls back to wpdb when mysql binary is unavailable + Given a WP install + And a fake-bin/mysql file: + """ + #!/bin/sh + exit 127 + """ + And a fake-bin/mariadb file: + """ + #!/bin/sh + exit 127 + """ + + When I run `chmod +x fake-bin/mysql fake-bin/mariadb` + And I run `env PATH={RUN_DIR}/fake-bin:$PATH wp db drop --yes --debug` + Then STDOUT should contain: + """ + Success: Database dropped. + """ + And STDERR should contain: + """ + Query via wpdb: + """ + + @require-mysql-or-mariadb + Scenario: Database reset falls back to wpdb when mysql binary is unavailable + Given a WP install + And a fake-bin/mysql file: + """ + #!/bin/sh + exit 127 + """ + And a fake-bin/mariadb file: + """ + #!/bin/sh + exit 127 + """ + + When I run `chmod +x fake-bin/mysql fake-bin/mariadb` + And I run `env PATH={RUN_DIR}/fake-bin:$PATH wp db reset --yes --debug` + Then STDOUT should contain: + """ + Success: Database reset. + """ + And STDERR should contain: + """ + Query via wpdb: + """ + @require-sqlite Scenario: SQLite DB CRUD operations Given a WP install diff --git a/src/DB_Command.php b/src/DB_Command.php index 44fbc84c..8a78a3aa 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1947,6 +1947,11 @@ protected function run_query( $query, $assoc_args = [] ) { WP_CLI::debug( "Query via wpdb: {$query}", 'db' ); $this->maybe_load_wpdb(); global $wpdb; + + if ( ! isset( $wpdb ) ) { + WP_CLI::error( 'WordPress database (wpdb) is not available. Please install MySQL or MariaDB client tools.' ); + } + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $result = $wpdb->query( $query ); if ( false === $result ) { From 9ea27c23549f17f9b769d08283c238d5e4821090 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Apr 2026 16:42:35 +0200 Subject: [PATCH 07/31] Apply suggestions from code review Co-authored-by: Pascal Birchler --- features/db.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/db.feature b/features/db.feature index 51207006..634c3f09 100644 --- a/features/db.feature +++ b/features/db.feature @@ -398,7 +398,7 @@ Feature: Perform database operations """ When I run `chmod +x fake-bin/mysql fake-bin/mariadb` - And I run `env PATH={RUN_DIR}/fake-bin:$PATH wp db drop --yes --debug` + And I try `env PATH={RUN_DIR}/fake-bin:$PATH wp db drop --yes --debug` Then STDOUT should contain: """ Success: Database dropped. @@ -423,7 +423,7 @@ Feature: Perform database operations """ When I run `chmod +x fake-bin/mysql fake-bin/mariadb` - And I run `env PATH={RUN_DIR}/fake-bin:$PATH wp db reset --yes --debug` + And I try `env PATH={RUN_DIR}/fake-bin:$PATH wp db reset --yes --debug` Then STDOUT should contain: """ Success: Database reset. From adc0d52a621387d895dd43e0a7456348f2991d1f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 09:42:22 +0000 Subject: [PATCH 08/31] Fix split_sql_statements() to handle backslash-escaped quotes inside SQL strings Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/DB_Command.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/DB_Command.php b/src/DB_Command.php index b8287283..401bbb4e 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -2645,6 +2645,15 @@ private function split_sql_statements( $sql ) { continue; } + // Handle backslash escaping inside quoted strings (e.g. \' or \"). + if ( '\\' === $char && ( $in_single_quote || $in_double_quote ) ) { + $current .= $char; + if ( $i + 1 < $length ) { + $current .= $sql[ ++$i ]; + } + continue; + } + if ( "'" === $char && ! $in_double_quote ) { $in_single_quote = ! $in_single_quote; } elseif ( '"' === $char && ! $in_single_quote ) { From 41b410d6ed7fa9b68554506b3ba8d8e85791daaa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 11:17:20 +0000 Subject: [PATCH 09/31] Fix: change I run to I try in db-import fallback test to allow STDERR debug output Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/db-import.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/db-import.feature b/features/db-import.feature index 0af8c19d..8f006f34 100644 --- a/features/db-import.feature +++ b/features/db-import.feature @@ -87,7 +87,7 @@ Feature: Import a WordPress database Then the wp_cli_test.sql file should exist When I run `chmod +x fake-bin/mysql fake-bin/mariadb` - And I run `env PATH={RUN_DIR}/fake-bin:$PATH wp db import wp_cli_test.sql --debug` + And I try `env PATH={RUN_DIR}/fake-bin:$PATH wp db import wp_cli_test.sql --debug` Then STDOUT should be: """ Success: Imported from 'wp_cli_test.sql'. From 72c546836991fa8a042e69caedb733be743a62e9 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Jul 2026 16:53:53 +0200 Subject: [PATCH 10/31] Fix issue after merge --- src/DB_Command.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 14035a5d..a92f26de 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -2461,8 +2461,7 @@ protected function is_mysql_binary_available() { static $available = null; if ( null === $available ) { - $binary = $this->get_mysql_command(); - $result = \WP_CLI\Process::create( '/usr/bin/env ' . escapeshellarg( $binary ) . ' --version', null, null )->run(); + $result = \WP_CLI\Process::create( Utils\get_mysql_binary_path() . ' --version', null, null )->run(); $available = 0 === $result->return_code; } From 62d9d98d881013fdaef64348c0d94a105dbd811e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:07:21 +0000 Subject: [PATCH 11/31] Fix run_query() WP 4.9 compatibility: use mysqli instead of wpdb Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/db.feature | 4 +-- src/DB_Command.php | 61 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/features/db.feature b/features/db.feature index 140f32aa..f4eb571f 100644 --- a/features/db.feature +++ b/features/db.feature @@ -405,7 +405,7 @@ Feature: Perform database operations """ And STDERR should contain: """ - Query via wpdb: + Query via mysqli: """ @require-mysql-or-mariadb @@ -430,7 +430,7 @@ Feature: Perform database operations """ And STDERR should contain: """ - Query via wpdb: + Query via mysqli: """ # Skipped on Windows due to persistent file locking issues when run via Behat. diff --git a/src/DB_Command.php b/src/DB_Command.php index a92f26de..33feed7c 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1959,6 +1959,51 @@ private static function get_create_query() { return $create_query; } + /** + * Run a single query via PHP's mysqli extension. + * + * Used as a fallback when the MySQL/MariaDB binary is not available. + * Creates a fresh connection per call so that DDL operations like + * DROP DATABASE followed by CREATE DATABASE work correctly. + * + * @param string $query SQL query to execute. + */ + protected function run_query_via_mysqli( $query ) { + $db_host = DB_HOST; + $port = null; + $socket = null; + + if ( ':' === substr( $db_host, 0, 1 ) ) { + $socket = substr( $db_host, 1 ); + $db_host = 'localhost'; + } else { + $parts = explode( ':', $db_host, 2 ); + $db_host = $parts[0]; + if ( isset( $parts[1] ) ) { + $port_or_socket = trim( $parts[1] ); + if ( '/' === $port_or_socket[0] ) { + $socket = $port_or_socket; + } else { + $port = (int) $port_or_socket; + } + } + } + + // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__mysqli -- direct mysqli required as wpdb fallback when mysql binary is unavailable. + $conn = new mysqli( $db_host, DB_USER, DB_PASSWORD, '', null !== $port ? $port : 3306, null !== $socket ? $socket : '' ); + + if ( $conn->connect_errno ) { + WP_CLI::error( sprintf( 'Failed to connect to the database: %s', $conn->connect_error ) ); + } + + if ( ! $conn->query( $query ) ) { + $error = $conn->error; + $conn->close(); + WP_CLI::error( sprintf( 'Query failed: %s', $error ) ); + } + $conn->close(); + } + /** * Run a single query via the 'mysql' binary. * @@ -1970,20 +2015,8 @@ private static function get_create_query() { */ protected function run_query( $query, $assoc_args = [] ) { if ( ! $this->is_mysql_binary_available() ) { - WP_CLI::debug( "Query via wpdb: {$query}", 'db' ); - $this->maybe_load_wpdb(); - global $wpdb; - - if ( ! isset( $wpdb ) ) { - WP_CLI::error( 'WordPress database (wpdb) is not available. Please install MySQL or MariaDB client tools.' ); - } - - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $result = $wpdb->query( $query ); - if ( false === $result ) { - // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags - WP_CLI::error( 'Query failed: ' . strip_tags( $wpdb->last_error ) ); - } + WP_CLI::debug( "Query via mysqli: {$query}", 'db' ); + $this->run_query_via_mysqli( $query ); return; } From 7e177eea916037276f4f1e2664eb09ad56cb534b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:08:28 +0000 Subject: [PATCH 12/31] Address code review: fix edge cases in run_query_via_mysqli() Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/DB_Command.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 33feed7c..89d10099 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1981,16 +1981,20 @@ protected function run_query_via_mysqli( $query ) { $db_host = $parts[0]; if ( isset( $parts[1] ) ) { $port_or_socket = trim( $parts[1] ); - if ( '/' === $port_or_socket[0] ) { + if ( '' !== $port_or_socket && '/' === $port_or_socket[0] ) { $socket = $port_or_socket; - } else { + } elseif ( '' !== $port_or_socket ) { $port = (int) $port_or_socket; } } } + // When using a socket, pass 0 for port. When a port is explicitly set, use it; otherwise use the default. + $port_val = null !== $port ? $port : 0; + $socket_val = null !== $socket ? $socket : ''; + // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__mysqli -- direct mysqli required as wpdb fallback when mysql binary is unavailable. - $conn = new mysqli( $db_host, DB_USER, DB_PASSWORD, '', null !== $port ? $port : 3306, null !== $socket ? $socket : '' ); + $conn = new mysqli( $db_host, DB_USER, DB_PASSWORD, '', $port_val, $socket_val ); if ( $conn->connect_errno ) { WP_CLI::error( sprintf( 'Failed to connect to the database: %s', $conn->connect_error ) ); From 60262b5cf2d00ab15846a9e43b041eb0418ca05c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:52:52 +0000 Subject: [PATCH 13/31] Restore wpdb fallback in run_query() and fix WP 4.9 compatibility Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/db.feature | 4 +- src/DB_Command.php | 99 +++++++++++++++++---------------------------- 2 files changed, 38 insertions(+), 65 deletions(-) diff --git a/features/db.feature b/features/db.feature index f4eb571f..140f32aa 100644 --- a/features/db.feature +++ b/features/db.feature @@ -405,7 +405,7 @@ Feature: Perform database operations """ And STDERR should contain: """ - Query via mysqli: + Query via wpdb: """ @require-mysql-or-mariadb @@ -430,7 +430,7 @@ Feature: Perform database operations """ And STDERR should contain: """ - Query via mysqli: + Query via wpdb: """ # Skipped on Windows due to persistent file locking issues when run via Behat. diff --git a/src/DB_Command.php b/src/DB_Command.php index 89d10099..f835fae7 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1959,55 +1959,6 @@ private static function get_create_query() { return $create_query; } - /** - * Run a single query via PHP's mysqli extension. - * - * Used as a fallback when the MySQL/MariaDB binary is not available. - * Creates a fresh connection per call so that DDL operations like - * DROP DATABASE followed by CREATE DATABASE work correctly. - * - * @param string $query SQL query to execute. - */ - protected function run_query_via_mysqli( $query ) { - $db_host = DB_HOST; - $port = null; - $socket = null; - - if ( ':' === substr( $db_host, 0, 1 ) ) { - $socket = substr( $db_host, 1 ); - $db_host = 'localhost'; - } else { - $parts = explode( ':', $db_host, 2 ); - $db_host = $parts[0]; - if ( isset( $parts[1] ) ) { - $port_or_socket = trim( $parts[1] ); - if ( '' !== $port_or_socket && '/' === $port_or_socket[0] ) { - $socket = $port_or_socket; - } elseif ( '' !== $port_or_socket ) { - $port = (int) $port_or_socket; - } - } - } - - // When using a socket, pass 0 for port. When a port is explicitly set, use it; otherwise use the default. - $port_val = null !== $port ? $port : 0; - $socket_val = null !== $socket ? $socket : ''; - - // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__mysqli -- direct mysqli required as wpdb fallback when mysql binary is unavailable. - $conn = new mysqli( $db_host, DB_USER, DB_PASSWORD, '', $port_val, $socket_val ); - - if ( $conn->connect_errno ) { - WP_CLI::error( sprintf( 'Failed to connect to the database: %s', $conn->connect_error ) ); - } - - if ( ! $conn->query( $query ) ) { - $error = $conn->error; - $conn->close(); - WP_CLI::error( sprintf( 'Query failed: %s', $error ) ); - } - $conn->close(); - } - /** * Run a single query via the 'mysql' binary. * @@ -2019,8 +1970,20 @@ protected function run_query_via_mysqli( $query ) { */ protected function run_query( $query, $assoc_args = [] ) { if ( ! $this->is_mysql_binary_available() ) { - WP_CLI::debug( "Query via mysqli: {$query}", 'db' ); - $this->run_query_via_mysqli( $query ); + WP_CLI::debug( "Query via wpdb: {$query}", 'db' ); + $this->maybe_load_wpdb(); + global $wpdb; + + if ( ! isset( $wpdb ) ) { + WP_CLI::error( 'WordPress database (wpdb) could not be initialized. Please ensure WordPress core files are properly installed.' ); + } + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $result = $wpdb->query( $query ); + if ( false === $result ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags + WP_CLI::error( 'Query failed: ' . strip_tags( $wpdb->last_error ) ); + } return; } @@ -2527,22 +2490,32 @@ protected function maybe_load_wpdb() { define( 'WP_CONTENT_DIR', ABSPATH . 'wp-content' ); } - // Load required WordPress files if not already loaded. + // Load prerequisite files if not already loaded. if ( ! function_exists( 'add_action' ) ) { - $required_files = [ - ABSPATH . WPINC . '/load.php', - ABSPATH . WPINC . '/compat.php', - ABSPATH . WPINC . '/plugin.php', - // Defines `wp_debug_backtrace_summary()` as used by wpdb. - ABSPATH . WPINC . '/functions.php', - ABSPATH . WPINC . '/class-wpdb.php', - ]; + foreach ( [ 'load.php', 'compat.php', 'plugin.php' ] as $file ) { + $path = ABSPATH . WPINC . '/' . $file; + if ( file_exists( $path ) ) { + require_once $path; + } + } + } - foreach ( $required_files as $required_file ) { - if ( file_exists( $required_file ) ) { - require_once $required_file; + // Load class-wpdb.php if the wpdb class is not yet available. + // This is checked independently of add_action, since WP-CLI's config loading + // may have already loaded plugin.php (defining add_action) without loading wpdb. + if ( ! class_exists( 'wpdb' ) ) { + // Defines `wp_debug_backtrace_summary()` as used by wpdb. + if ( ! function_exists( 'wp_debug_backtrace_summary' ) ) { + $functions_file = ABSPATH . WPINC . '/functions.php'; + if ( file_exists( $functions_file ) ) { + require_once $functions_file; } } + + $wpdb_file = ABSPATH . WPINC . '/class-wpdb.php'; + if ( file_exists( $wpdb_file ) ) { + require_once $wpdb_file; + } } // Load db.php drop-in if it exists (e.g., HyperDB or other custom drivers). From 23355d685dd921f5597dbdb71aea624b6d86a3f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 10:25:33 +0000 Subject: [PATCH 14/31] Fix WP 4.9: use run_query_via_mysqli() for drop/reset, keep wpdb for query/import Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/db.feature | 4 +-- src/DB_Command.php | 66 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/features/db.feature b/features/db.feature index 140f32aa..f4eb571f 100644 --- a/features/db.feature +++ b/features/db.feature @@ -405,7 +405,7 @@ Feature: Perform database operations """ And STDERR should contain: """ - Query via wpdb: + Query via mysqli: """ @require-mysql-or-mariadb @@ -430,7 +430,7 @@ Feature: Perform database operations """ And STDERR should contain: """ - Query via wpdb: + Query via mysqli: """ # Skipped on Windows due to persistent file locking issues when run via Behat. diff --git a/src/DB_Command.php b/src/DB_Command.php index f835fae7..aaee572f 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1959,6 +1959,56 @@ private static function get_create_query() { return $create_query; } + /** + * Run a single DDL query (e.g. DROP/CREATE DATABASE) using PHP's mysqli extension. + * + * Used as a fallback when the mysql/mariadb CLI binary is not available. + * Creates a fresh connection per call so that DDL operations like + * DROP DATABASE followed by CREATE DATABASE work correctly. + * + * @param string $query SQL query to execute. + */ + protected function run_query_via_mysqli( $query ) { + $db_host = DB_HOST; + $port = null; + $socket = null; + + if ( ':' === substr( $db_host, 0, 1 ) ) { + $socket = substr( $db_host, 1 ); + $db_host = 'localhost'; + } else { + $parts = explode( ':', $db_host, 2 ); + $db_host = $parts[0]; + if ( isset( $parts[1] ) ) { + $port_or_socket = trim( $parts[1] ); + if ( '' !== $port_or_socket && '/' === $port_or_socket[0] ) { + $socket = $port_or_socket; + } elseif ( '' !== $port_or_socket ) { + $port = (int) $port_or_socket; + } + } + } + + // When using a socket, pass 0 for port. When a port is explicitly set, use it; otherwise use the default. + $port_value = null !== $port ? $port : 0; + $socket_value = null !== $socket ? $socket : ''; + + // No database is selected intentionally — DDL operations like DROP/CREATE DATABASE work at the server level. + // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__mysqli -- direct mysqli required as wpdb fallback when mysql binary is unavailable. + $conn = new mysqli( $db_host, DB_USER, DB_PASSWORD, '', $port_value, $socket_value ); + + if ( $conn->connect_errno ) { + WP_CLI::error( sprintf( 'Failed to connect to the database: %s', $conn->connect_error ) ); + } + + if ( ! $conn->query( $query ) ) { + $error = $conn->error; + $conn->close(); + WP_CLI::error( sprintf( 'Query failed: %s', $error ) ); + } + $conn->close(); + } + /** * Run a single query via the 'mysql' binary. * @@ -1970,20 +2020,8 @@ private static function get_create_query() { */ protected function run_query( $query, $assoc_args = [] ) { if ( ! $this->is_mysql_binary_available() ) { - WP_CLI::debug( "Query via wpdb: {$query}", 'db' ); - $this->maybe_load_wpdb(); - global $wpdb; - - if ( ! isset( $wpdb ) ) { - WP_CLI::error( 'WordPress database (wpdb) could not be initialized. Please ensure WordPress core files are properly installed.' ); - } - - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $result = $wpdb->query( $query ); - if ( false === $result ) { - // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags - WP_CLI::error( 'Query failed: ' . strip_tags( $wpdb->last_error ) ); - } + WP_CLI::debug( "Query via mysqli: {$query}", 'db' ); + $this->run_query_via_mysqli( $query ); return; } From 6414ec596a4e196eb501136e9d486bf2001ef533 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:34:30 +0000 Subject: [PATCH 15/31] Replace maybe_load_wpdb() with direct mysqli in wpdb_query() and wpdb_import() Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/DB_Command.php | 213 ++++++++++++++++----------------------------- 1 file changed, 77 insertions(+), 136 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index aaee572f..0fc61289 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -606,7 +606,6 @@ public function query( $args, $assoc_args ) { } WP_CLI::debug( 'MySQL/MariaDB binary not available, falling back to wpdb.', 'db' ); - $this->maybe_load_wpdb(); $this->wpdb_query( $query, $assoc_args ); return; } @@ -958,7 +957,6 @@ public function import( $args, $assoc_args ) { } WP_CLI::debug( 'MySQL/MariaDB binary not available, falling back to wpdb for import.', 'db' ); - $this->maybe_load_wpdb(); $this->wpdb_import( (string) $sql_content, $assoc_args ); WP_CLI::success( sprintf( "Imported from '%s'.", $result_file ) ); return; @@ -1969,37 +1967,8 @@ private static function get_create_query() { * @param string $query SQL query to execute. */ protected function run_query_via_mysqli( $query ) { - $db_host = DB_HOST; - $port = null; - $socket = null; - - if ( ':' === substr( $db_host, 0, 1 ) ) { - $socket = substr( $db_host, 1 ); - $db_host = 'localhost'; - } else { - $parts = explode( ':', $db_host, 2 ); - $db_host = $parts[0]; - if ( isset( $parts[1] ) ) { - $port_or_socket = trim( $parts[1] ); - if ( '' !== $port_or_socket && '/' === $port_or_socket[0] ) { - $socket = $port_or_socket; - } elseif ( '' !== $port_or_socket ) { - $port = (int) $port_or_socket; - } - } - } - - // When using a socket, pass 0 for port. When a port is explicitly set, use it; otherwise use the default. - $port_value = null !== $port ? $port : 0; - $socket_value = null !== $socket ? $socket : ''; - - // No database is selected intentionally — DDL operations like DROP/CREATE DATABASE work at the server level. - // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__mysqli -- direct mysqli required as wpdb fallback when mysql binary is unavailable. - $conn = new mysqli( $db_host, DB_USER, DB_PASSWORD, '', $port_value, $socket_value ); - - if ( $conn->connect_errno ) { - WP_CLI::error( sprintf( 'Failed to connect to the database: %s', $conn->connect_error ) ); - } + // No database selected — DDL operations like DROP/CREATE DATABASE work at the server level. + $conn = $this->get_db_connection( false ); if ( ! $conn->query( $query ) ) { $error = $conn->error; @@ -2507,118 +2476,101 @@ protected function is_mysql_binary_available() { } /** - * Load WordPress's wpdb if not already available. + * Open a mysqli connection using the WordPress database credentials. + * + * Used as a fallback when the mysql/mariadb binary is not available. + * Parses DB_HOST the same way WordPress does (host:port, host:/socket, :/socket). * - * Loads the minimal required WordPress files to make $wpdb available, - * including any db.php drop-in (e.g., HyperDB or other custom drivers). + * @param bool $select_db Whether to select DB_NAME on connect. Pass false for + * server-level DDL (DROP/CREATE DATABASE). + * @return mysqli Connected mysqli instance. Calls WP_CLI::error() on failure. */ - protected function maybe_load_wpdb() { - global $wpdb; - - if ( isset( $wpdb ) ) { - return; - } - - if ( ! defined( 'WPINC' ) ) { - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound - define( 'WPINC', 'wp-includes' ); - } - - if ( ! defined( 'WP_CONTENT_DIR' ) ) { - define( 'WP_CONTENT_DIR', ABSPATH . 'wp-content' ); - } + protected function get_db_connection( $select_db = true ) { + $db_host = DB_HOST; + $port = null; + $socket = null; - // Load prerequisite files if not already loaded. - if ( ! function_exists( 'add_action' ) ) { - foreach ( [ 'load.php', 'compat.php', 'plugin.php' ] as $file ) { - $path = ABSPATH . WPINC . '/' . $file; - if ( file_exists( $path ) ) { - require_once $path; + if ( ':' === substr( $db_host, 0, 1 ) ) { + $socket = substr( $db_host, 1 ); + $db_host = 'localhost'; + } else { + $parts = explode( ':', $db_host, 2 ); + $db_host = $parts[0]; + if ( isset( $parts[1] ) ) { + $port_or_socket = trim( $parts[1] ); + if ( '' !== $port_or_socket && '/' === $port_or_socket[0] ) { + $socket = $port_or_socket; + } elseif ( '' !== $port_or_socket ) { + $port = (int) $port_or_socket; } } } - // Load class-wpdb.php if the wpdb class is not yet available. - // This is checked independently of add_action, since WP-CLI's config loading - // may have already loaded plugin.php (defining add_action) without loading wpdb. - if ( ! class_exists( 'wpdb' ) ) { - // Defines `wp_debug_backtrace_summary()` as used by wpdb. - if ( ! function_exists( 'wp_debug_backtrace_summary' ) ) { - $functions_file = ABSPATH . WPINC . '/functions.php'; - if ( file_exists( $functions_file ) ) { - require_once $functions_file; - } - } + $port_value = null !== $port ? $port : 0; + $socket_value = null !== $socket ? $socket : ''; + $database = $select_db ? DB_NAME : ''; - $wpdb_file = ABSPATH . WPINC . '/class-wpdb.php'; - if ( file_exists( $wpdb_file ) ) { - require_once $wpdb_file; - } - } + // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__mysqli -- direct mysqli required as fallback when mysql binary is unavailable. + $conn = new mysqli( $db_host, DB_USER, DB_PASSWORD, $database, $port_value, $socket_value ); - // Load db.php drop-in if it exists (e.g., HyperDB or other custom drivers). - $db_dropin_path = WP_CONTENT_DIR . '/db.php'; - if ( file_exists( $db_dropin_path ) && ! $this->is_sqlite() ) { - require_once $db_dropin_path; + if ( $conn->connect_errno ) { + WP_CLI::error( sprintf( 'Failed to connect to the database: %s', $conn->connect_error ) ); } - // If $wpdb is still not set (e.g. no drop-in), create a new instance using the DB credentials from wp-config.php. - if ( ! isset( $GLOBALS['wpdb'] ) && class_exists( 'wpdb' ) ) { - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $wpdb = new wpdb( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST ); - if ( isset( $GLOBALS['table_prefix'] ) && is_string( $GLOBALS['table_prefix'] ) ) { - $wpdb->set_prefix( $GLOBALS['table_prefix'] ); - } - } + return $conn; } /** - * Execute a query against the database using wpdb. + * Execute a query against the database using mysqli. * * Used as a fallback when the mysql/mariadb binary is not available. + * Outputs results in the same tab-separated format as the mysql binary. * * @param string $query SQL query to execute. * @param array $assoc_args Associative arguments. */ protected function wpdb_query( $query, $assoc_args = [] ) { - global $wpdb; - - if ( ! isset( $wpdb ) || ! ( $wpdb instanceof wpdb ) ) { - WP_CLI::error( 'WordPress database (wpdb) is not available. Please install MySQL or MariaDB client tools.' ); - } + $conn = $this->get_db_connection(); - $skip_column_names = Utils\get_flag_value( $assoc_args, 'skip-column-names', false ); - - $is_row_modifying_query = preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE(?!\s*\()|LOAD DATA)\b/i', $query ); + $skip_column_names = Utils\get_flag_value( $assoc_args, 'skip-column-names', false ); + $is_row_modifying_query = (bool) preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE(?!\s*\()|LOAD DATA)\b/i', $query ); if ( $is_row_modifying_query ) { - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $affected_rows = $wpdb->query( $query ); - if ( false === $affected_rows ) { - // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags - WP_CLI::error( 'Query failed: ' . strip_tags( $wpdb->last_error ) ); + if ( ! $conn->query( $query ) ) { + $error = $conn->error; + $conn->close(); + WP_CLI::error( "Query failed: {$error}" ); } + $affected_rows = $conn->affected_rows; + $conn->close(); WP_CLI::success( "Query succeeded. Rows affected: {$affected_rows}" ); } else { - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $results = $wpdb->get_results( $query, ARRAY_A ); - - if ( $wpdb->last_error ) { - // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags - WP_CLI::error( 'Query failed: ' . strip_tags( $wpdb->last_error ) ); + $result = $conn->query( $query ); + if ( ! $result ) { + $error = $conn->error; + $conn->close(); + WP_CLI::error( "Query failed: {$error}" ); } - if ( empty( $results ) ) { - return; + if ( $result instanceof mysqli_result ) { + $fields = $result->fetch_fields(); + $headers = $fields ? array_column( (array) $fields, 'name' ) : []; + if ( ! $skip_column_names && ! empty( $headers ) ) { + WP_CLI::line( implode( "\t", $headers ) ); + } + $all_rows = $result->fetch_all( MYSQLI_NUM ); + foreach ( $all_rows as $row ) { + WP_CLI::line( implode( "\t", array_map( 'strval', $row ) ) ); + } + $result->free(); } - $headers = array_keys( $results[0] ); - $this->display_query_results( $headers, $results, $skip_column_names ); + $conn->close(); } } /** - * Import SQL content into the database using wpdb. + * Import SQL content into the database using mysqli. * * Used as a fallback when the mysql/mariadb binary is not available. * @@ -2626,21 +2578,14 @@ protected function wpdb_query( $query, $assoc_args = [] ) { * @param array $assoc_args Associative arguments. */ protected function wpdb_import( $sql_content, $assoc_args = [] ) { - global $wpdb; - - if ( ! isset( $wpdb ) || ! ( $wpdb instanceof wpdb ) ) { - WP_CLI::error( 'WordPress database (wpdb) is not available. Please install MySQL or MariaDB client tools.' ); - } + $conn = $this->get_db_connection(); $skip_optimization = Utils\get_flag_value( $assoc_args, 'skip-optimization', false ); if ( ! $skip_optimization ) { - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $wpdb->query( 'SET autocommit = 0' ); - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $wpdb->query( 'SET unique_checks = 0' ); - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $wpdb->query( 'SET foreign_key_checks = 0' ); + $conn->query( 'SET autocommit = 0' ); + $conn->query( 'SET unique_checks = 0' ); + $conn->query( 'SET foreign_key_checks = 0' ); } $statements = $this->split_sql_statements( $sql_content ); @@ -2650,28 +2595,24 @@ protected function wpdb_import( $sql_content, $assoc_args = [] ) { if ( '' === $statement ) { continue; } - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $result = $wpdb->query( $statement ); - if ( false === $result ) { + if ( ! $conn->query( $statement ) ) { + $error = $conn->error; if ( ! $skip_optimization ) { - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $wpdb->query( 'ROLLBACK' ); + $conn->query( 'ROLLBACK' ); } - // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags - WP_CLI::error( 'Import failed: ' . strip_tags( $wpdb->last_error ) ); + $conn->close(); + WP_CLI::error( "Import failed: {$error}" ); } } if ( ! $skip_optimization ) { - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $wpdb->query( 'COMMIT' ); - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $wpdb->query( 'SET autocommit = 1' ); - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $wpdb->query( 'SET unique_checks = 1' ); - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $wpdb->query( 'SET foreign_key_checks = 1' ); + $conn->query( 'COMMIT' ); + $conn->query( 'SET autocommit = 1' ); + $conn->query( 'SET unique_checks = 1' ); + $conn->query( 'SET foreign_key_checks = 1' ); } + + $conn->close(); } /** From e1d6e78a1607e2be570ce2565eca600516e2f469 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:35:16 +0000 Subject: [PATCH 16/31] Fix code review: remove redundant cast, handle NULL as 'NULL' in query output Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/DB_Command.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 0fc61289..65770643 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -2554,13 +2554,23 @@ protected function wpdb_query( $query, $assoc_args = [] ) { if ( $result instanceof mysqli_result ) { $fields = $result->fetch_fields(); - $headers = $fields ? array_column( (array) $fields, 'name' ) : []; + $headers = $fields ? array_column( $fields, 'name' ) : []; if ( ! $skip_column_names && ! empty( $headers ) ) { WP_CLI::line( implode( "\t", $headers ) ); } $all_rows = $result->fetch_all( MYSQLI_NUM ); foreach ( $all_rows as $row ) { - WP_CLI::line( implode( "\t", array_map( 'strval', $row ) ) ); + WP_CLI::line( + implode( + "\t", + array_map( + static function ( $v ) { + return null === $v ? 'NULL' : (string) $v; + }, + $row + ) + ) + ); } $result->free(); } From b6489cbc6b2cbead9aa71cf26d5a0cdb0eb5a88d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:29:23 +0000 Subject: [PATCH 17/31] Fix split_sql_statements() to execute MySQL conditional comments (/*!...*/) Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/DB_Command.php | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 65770643..d5fae90f 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -2635,13 +2635,14 @@ protected function wpdb_import( $sql_content, $assoc_args = [] ) { * @return string[] Array of individual SQL statements. */ private function split_sql_statements( $sql ) { - $statements = []; - $current = ''; - $in_single_quote = false; - $in_double_quote = false; - $in_comment = false; - $in_line_comment = false; - $length = strlen( $sql ); + $statements = []; + $current = ''; + $in_single_quote = false; + $in_double_quote = false; + $in_comment = false; + $in_line_comment = false; + $in_conditional_comment = false; + $length = strlen( $sql ); for ( $i = 0; $i < $length; $i++ ) { $char = $sql[ $i ]; @@ -2662,9 +2663,34 @@ private function split_sql_statements( $sql ) { continue; } + // Handle end of MySQL conditional comment (/*!...*/): resume normal parsing. + if ( $in_conditional_comment && ! $in_single_quote && ! $in_double_quote ) { + if ( '*' === $char && '/' === $next ) { + $in_conditional_comment = false; + ++$i; + continue; + } + // Fall through: treat content as regular SQL (handled below). + } + if ( '/' === $char && '*' === $next && ! $in_single_quote && ! $in_double_quote ) { - $in_comment = true; - ++$i; + $peek = ( $i + 2 < $length ) ? $sql[ $i + 2 ] : ''; + if ( '!' === $peek ) { + // MySQL conditional comment (/*!...*/): execute its SQL content. + $in_conditional_comment = true; + $i += 2; // skip /*! + // Skip optional version digits (e.g. 40101 in /*!40101 SET ... */). + while ( $i + 1 < $length && ctype_digit( $sql[ $i + 1 ] ) ) { + ++$i; + } + // Skip one space following version digits, if present. + if ( $i + 1 < $length && ' ' === $sql[ $i + 1 ] ) { + ++$i; + } + } else { + $in_comment = true; + ++$i; + } continue; } From ec70aa979d7af5ed7949f66a7163aa6ed6372b70 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:30:51 +0000 Subject: [PATCH 18/31] Improve split_sql_statements: rename $peek, clarify version-digit loop comments Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/DB_Command.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index d5fae90f..a597c5b1 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -2674,19 +2674,21 @@ private function split_sql_statements( $sql ) { } if ( '/' === $char && '*' === $next && ! $in_single_quote && ! $in_double_quote ) { - $peek = ( $i + 2 < $length ) ? $sql[ $i + 2 ] : ''; - if ( '!' === $peek ) { + $char_after_asterisk = ( $i + 2 < $length ) ? $sql[ $i + 2 ] : ''; + if ( '!' === $char_after_asterisk ) { // MySQL conditional comment (/*!...*/): execute its SQL content. $in_conditional_comment = true; - $i += 2; // skip /*! - // Skip optional version digits (e.g. 40101 in /*!40101 SET ... */). + $i += 2; // skip past /*!; $i now points at ! + // Advance past optional version digits (e.g. "40101" in /*!40101 SET ... */). + // Loop checks the NEXT character so $i ends at the last digit. while ( $i + 1 < $length && ctype_digit( $sql[ $i + 1 ] ) ) { ++$i; } - // Skip one space following version digits, if present. + // Skip one space following the version digits, if present. if ( $i + 1 < $length && ' ' === $sql[ $i + 1 ] ) { ++$i; } + // The for-loop's own ++$i then lands on the first SQL char. } else { $in_comment = true; ++$i; From 56aa39b2e9f45b20dcea26e65a7884491a71c0aa Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 22 Jul 2026 14:19:23 +0200 Subject: [PATCH 19/31] Remove mysqli fallback in favor of wpdb --- src/DB_Command.php | 240 +++++++++++++++++++++++++-------------------- 1 file changed, 131 insertions(+), 109 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index e7262b50..fd88387d 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1974,27 +1974,6 @@ private static function get_create_query() { return $create_query; } - /** - * Run a single DDL query (e.g. DROP/CREATE DATABASE) using PHP's mysqli extension. - * - * Used as a fallback when the mysql/mariadb CLI binary is not available. - * Creates a fresh connection per call so that DDL operations like - * DROP DATABASE followed by CREATE DATABASE work correctly. - * - * @param string $query SQL query to execute. - */ - protected function run_query_via_mysqli( $query ) { - // No database selected — DDL operations like DROP/CREATE DATABASE work at the server level. - $conn = $this->get_db_connection( false ); - - if ( ! $conn->query( $query ) ) { - $error = $conn->error; - $conn->close(); - WP_CLI::error( sprintf( 'Query failed: %s', $error ) ); - } - $conn->close(); - } - /** * Run a single query via the 'mysql' binary. * @@ -2005,12 +1984,6 @@ protected function run_query_via_mysqli( $query ) { * @param array $assoc_args Optional. Associative array of arguments. */ protected function run_query( $query, $assoc_args = [] ) { - if ( ! $this->is_mysql_binary_available() ) { - WP_CLI::debug( "Query via mysqli: {$query}", 'db' ); - $this->run_query_via_mysqli( $query ); - return; - } - WP_CLI::debug( "Query: {$query}", 'db' ); $mysql_args = array_merge( @@ -2479,52 +2452,69 @@ protected function is_mysql_binary_available() { } /** - * Open a mysqli connection using the WordPress database credentials. - * - * Used as a fallback when the mysql/mariadb binary is not available. - * Parses DB_HOST the same way WordPress does (host:port, host:/socket, :/socket). + * Load WordPress's wpdb if not already available. * - * @param bool $select_db Whether to select DB_NAME on connect. Pass false for - * server-level DDL (DROP/CREATE DATABASE). - * @return mysqli Connected mysqli instance. Calls WP_CLI::error() on failure. + * Loads the minimal required WordPress files to make $wpdb available, + * including any db.php drop-in (e.g., HyperDB or other custom drivers). */ - protected function get_db_connection( $select_db = true ) { - $db_host = DB_HOST; - $port = null; - $socket = null; - - if ( ':' === substr( $db_host, 0, 1 ) ) { - $socket = substr( $db_host, 1 ); - $db_host = 'localhost'; - } else { - $parts = explode( ':', $db_host, 2 ); - $db_host = $parts[0]; - if ( isset( $parts[1] ) ) { - $port_or_socket = trim( $parts[1] ); - if ( '' !== $port_or_socket && '/' === $port_or_socket[0] ) { - $socket = $port_or_socket; - } elseif ( '' !== $port_or_socket ) { - $port = (int) $port_or_socket; + protected function maybe_load_wpdb() { + global $wpdb; + + if ( isset( $wpdb ) && $wpdb instanceof wpdb ) { + return; + } + + if ( ! defined( 'WPINC' ) ) { + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound + define( 'WPINC', 'wp-includes' ); + } + + if ( ! defined( 'WP_CONTENT_DIR' ) ) { + define( 'WP_CONTENT_DIR', ABSPATH . 'wp-content' ); + } + + // Load prerequisite files if not already loaded. + if ( ! function_exists( 'add_action' ) ) { + foreach ( [ 'load.php', 'compat.php', 'plugin.php' ] as $file ) { + $path = ABSPATH . WPINC . '/' . $file; + if ( file_exists( $path ) ) { + require_once $path; } } } - $port_value = null !== $port ? $port : 0; - $socket_value = null !== $socket ? $socket : ''; - $database = $select_db ? DB_NAME : ''; + // Load class-wpdb.php if the wpdb class is not yet available. + if ( ! class_exists( 'wpdb' ) ) { + if ( ! function_exists( 'wp_debug_backtrace_summary' ) ) { + $functions_file = ABSPATH . WPINC . '/functions.php'; + if ( file_exists( $functions_file ) ) { + require_once $functions_file; + } + } - // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__mysqli -- direct mysqli required as fallback when mysql binary is unavailable. - $conn = new mysqli( $db_host, DB_USER, DB_PASSWORD, $database, $port_value, $socket_value ); + $wpdb_file = ABSPATH . WPINC . '/class-wpdb.php'; + if ( file_exists( $wpdb_file ) ) { + require_once $wpdb_file; + } + } - if ( $conn->connect_errno ) { - WP_CLI::error( sprintf( 'Failed to connect to the database: %s', $conn->connect_error ) ); + // Load db.php drop-in if it exists (e.g., HyperDB or other custom drivers). + $db_dropin_path = WP_CONTENT_DIR . '/db.php'; + if ( file_exists( $db_dropin_path ) && ! $this->is_sqlite() ) { + require_once $db_dropin_path; } - return $conn; + // If $wpdb is still not set (e.g. no drop-in), create a new instance using the DB credentials from wp-config.php. + if ( ! isset( $GLOBALS['wpdb'] ) && class_exists( 'wpdb' ) ) { + $table_prefix = isset( $GLOBALS['table_prefix'] ) && is_string( $GLOBALS['table_prefix'] ) ? $GLOBALS['table_prefix'] : 'wp_'; + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $wpdb = new wpdb( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST ); + $wpdb->set_prefix( $table_prefix ); + } } /** - * Execute a query against the database using mysqli. + * Execute a query against the database using wpdb. * * Used as a fallback when the mysql/mariadb binary is not available. * Outputs results in the same tab-separated format as the mysql binary. @@ -2533,57 +2523,65 @@ protected function get_db_connection( $select_db = true ) { * @param array $assoc_args Associative arguments. */ protected function wpdb_query( $query, $assoc_args = [] ) { - $conn = $this->get_db_connection(); + $this->maybe_load_wpdb(); + global $wpdb; + + if ( ! isset( $wpdb ) || ! ( $wpdb instanceof wpdb ) ) { + WP_CLI::error( 'WordPress database (wpdb) is not available. Please install MySQL or MariaDB client tools.' ); + } $skip_column_names = Utils\get_flag_value( $assoc_args, 'skip-column-names', false ); $is_row_modifying_query = (bool) preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE(?!\s*\()|LOAD DATA)\b/i', $query ); if ( $is_row_modifying_query ) { - if ( ! $conn->query( $query ) ) { - $error = $conn->error; - $conn->close(); - WP_CLI::error( "Query failed: {$error}" ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $affected_rows = $wpdb->query( $query ); + if ( false === $affected_rows ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags + WP_CLI::error( 'Query failed: ' . strip_tags( $wpdb->last_error ) ); } - $affected_rows = $conn->affected_rows; - $conn->close(); WP_CLI::success( "Query succeeded. Rows affected: {$affected_rows}" ); } else { - $result = $conn->query( $query ); - if ( ! $result ) { - $error = $conn->error; - $conn->close(); - WP_CLI::error( "Query failed: {$error}" ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $results = $wpdb->get_results( $query, ARRAY_A ); + + if ( $wpdb->last_error ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags + WP_CLI::error( 'Query failed: ' . strip_tags( $wpdb->last_error ) ); } - if ( $result instanceof mysqli_result ) { - $fields = $result->fetch_fields(); - $headers = $fields ? array_column( $fields, 'name' ) : []; - if ( ! $skip_column_names && ! empty( $headers ) ) { - WP_CLI::line( implode( "\t", $headers ) ); - } - $all_rows = $result->fetch_all( MYSQLI_NUM ); - foreach ( $all_rows as $row ) { - WP_CLI::line( - implode( - "\t", - array_map( - static function ( $v ) { - return null === $v ? 'NULL' : (string) $v; - }, - $row - ) - ) - ); - } - $result->free(); + if ( empty( $results ) ) { + return; } - $conn->close(); + $headers = array_keys( $results[0] ); + if ( ! $skip_column_names && ! empty( $headers ) ) { + WP_CLI::line( implode( "\t", $headers ) ); + } + foreach ( $results as $row ) { + WP_CLI::line( + implode( + "\t", + array_map( + static function ( $v ) { + if ( null === $v ) { + return 'NULL'; + } + if ( is_scalar( $v ) ) { + return (string) $v; + } + return ''; + }, + array_values( $row ) + ) + ) + ); + } } } /** - * Import SQL content into the database using mysqli. + * Import SQL content into the database using wpdb. * * Used as a fallback when the mysql/mariadb binary is not available. * @@ -2591,14 +2589,24 @@ static function ( $v ) { * @param array $assoc_args Associative arguments. */ protected function wpdb_import( $sql_content, $assoc_args = [] ) { - $conn = $this->get_db_connection(); + $this->maybe_load_wpdb(); + global $wpdb; + + if ( ! isset( $wpdb ) || ! ( $wpdb instanceof wpdb ) ) { + WP_CLI::error( 'WordPress database (wpdb) is not available. Please install MySQL or MariaDB client tools.' ); + } + + $suppress = $wpdb->suppress_errors( true ); $skip_optimization = Utils\get_flag_value( $assoc_args, 'skip-optimization', false ); if ( ! $skip_optimization ) { - $conn->query( 'SET autocommit = 0' ); - $conn->query( 'SET unique_checks = 0' ); - $conn->query( 'SET foreign_key_checks = 0' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'SET autocommit = 0' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'SET unique_checks = 0' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'SET foreign_key_checks = 0' ); } $statements = $this->split_sql_statements( $sql_content ); @@ -2608,24 +2616,38 @@ protected function wpdb_import( $sql_content, $assoc_args = [] ) { if ( '' === $statement ) { continue; } - if ( ! $conn->query( $statement ) ) { - $error = $conn->error; + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $result = $wpdb->query( $statement ); + if ( false === $result ) { + $error = $wpdb->last_error; + + // Ignore privilege/permission errors for SET statements (e.g. SQL_LOG_BIN, GTID) in dumps when user lacks SUPER admin rights. + if ( 0 === stripos( ltrim( $statement ), 'SET ' ) && ( false !== strpos( $error, 'Access denied' ) || false !== strpos( $error, 'privilege' ) ) ) { + continue; + } + if ( ! $skip_optimization ) { - $conn->query( 'ROLLBACK' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'ROLLBACK' ); } - $conn->close(); - WP_CLI::error( "Import failed: {$error}" ); + $wpdb->suppress_errors( $suppress ); + // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags + WP_CLI::error( 'Import failed: ' . strip_tags( $error ) ); } } if ( ! $skip_optimization ) { - $conn->query( 'COMMIT' ); - $conn->query( 'SET autocommit = 1' ); - $conn->query( 'SET unique_checks = 1' ); - $conn->query( 'SET foreign_key_checks = 1' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'COMMIT' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'SET autocommit = 1' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'SET unique_checks = 1' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'SET foreign_key_checks = 1' ); } - $conn->close(); + $wpdb->suppress_errors( $suppress ); } /** From 79dd99df3a5610c0c87918430028307ae8332b3a Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 22 Jul 2026 14:19:29 +0200 Subject: [PATCH 20/31] Update tests for wpdb fallback --- features/db-import.feature | 2 +- features/db.feature | 50 -------------------------------------- 2 files changed, 1 insertion(+), 51 deletions(-) diff --git a/features/db-import.feature b/features/db-import.feature index 7b69b468..165ff4e0 100644 --- a/features/db-import.feature +++ b/features/db-import.feature @@ -83,7 +83,7 @@ Feature: Import a WordPress database exit 127 """ - When I run `wp db export wp_cli_test.sql` + When I try `wp db export wp_cli_test.sql` Then the wp_cli_test.sql file should exist When I run `chmod +x fake-bin/mysql fake-bin/mariadb` diff --git a/features/db.feature b/features/db.feature index e31fcd6a..45651d44 100644 --- a/features/db.feature +++ b/features/db.feature @@ -387,56 +387,6 @@ Feature: Perform database operations Query succeeded. Rows affected: 1 """ - @require-mysql-or-mariadb - Scenario: Database drop falls back to wpdb when mysql binary is unavailable - Given a WP install - And a fake-bin/mysql file: - """ - #!/bin/sh - exit 127 - """ - And a fake-bin/mariadb file: - """ - #!/bin/sh - exit 127 - """ - - When I run `chmod +x fake-bin/mysql fake-bin/mariadb` - And I try `env PATH={RUN_DIR}/fake-bin:$PATH wp db drop --yes --debug` - Then STDOUT should contain: - """ - Success: Database dropped. - """ - And STDERR should contain: - """ - Query via mysqli: - """ - - @require-mysql-or-mariadb - Scenario: Database reset falls back to wpdb when mysql binary is unavailable - Given a WP install - And a fake-bin/mysql file: - """ - #!/bin/sh - exit 127 - """ - And a fake-bin/mariadb file: - """ - #!/bin/sh - exit 127 - """ - - When I run `chmod +x fake-bin/mysql fake-bin/mariadb` - And I try `env PATH={RUN_DIR}/fake-bin:$PATH wp db reset --yes --debug` - Then STDOUT should contain: - """ - Success: Database reset. - """ - And STDERR should contain: - """ - Query via mysqli: - """ - # Skipped on Windows due to persistent file locking issues when run via Behat. @require-sqlite @skip-windows Scenario: SQLite DB CRUD operations From 6b430fba1668f47106809e250b45ac740a803854 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 22 Jul 2026 14:48:34 +0200 Subject: [PATCH 21/31] Fix WP 4.9 functions.php require issue in maybe_load_wpdb and add debug logging --- src/DB_Command.php | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index fd88387d..adf5095b 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -2473,44 +2473,41 @@ protected function maybe_load_wpdb() { define( 'WP_CONTENT_DIR', ABSPATH . 'wp-content' ); } - // Load prerequisite files if not already loaded. - if ( ! function_exists( 'add_action' ) ) { - foreach ( [ 'load.php', 'compat.php', 'plugin.php' ] as $file ) { - $path = ABSPATH . WPINC . '/' . $file; - if ( file_exists( $path ) ) { - require_once $path; - } - } - } - - // Load class-wpdb.php if the wpdb class is not yet available. - if ( ! class_exists( 'wpdb' ) ) { - if ( ! function_exists( 'wp_debug_backtrace_summary' ) ) { - $functions_file = ABSPATH . WPINC . '/functions.php'; - if ( file_exists( $functions_file ) ) { - require_once $functions_file; - } - } + // Load prerequisite WordPress files if not already loaded. + $prereqs = [ + 'load.php' => ABSPATH . WPINC . '/load.php', + 'compat.php' => ABSPATH . WPINC . '/compat.php', + 'plugin.php' => ABSPATH . WPINC . '/plugin.php', + 'class-wp-error.php' => ABSPATH . WPINC . '/class-wp-error.php', + 'class-wpdb.php' => ABSPATH . WPINC . '/class-wpdb.php', + ]; - $wpdb_file = ABSPATH . WPINC . '/class-wpdb.php'; - if ( file_exists( $wpdb_file ) ) { - require_once $wpdb_file; + foreach ( $prereqs as $name => $path ) { + if ( file_exists( $path ) ) { + WP_CLI::debug( "Loading WordPress core file: {$name}", 'db' ); + require_once $path; } } // Load db.php drop-in if it exists (e.g., HyperDB or other custom drivers). $db_dropin_path = WP_CONTENT_DIR . '/db.php'; if ( file_exists( $db_dropin_path ) && ! $this->is_sqlite() ) { + WP_CLI::debug( "Loading db.php drop-in from {$db_dropin_path}", 'db' ); require_once $db_dropin_path; } // If $wpdb is still not set (e.g. no drop-in), create a new instance using the DB credentials from wp-config.php. if ( ! isset( $GLOBALS['wpdb'] ) && class_exists( 'wpdb' ) ) { + WP_CLI::debug( 'Instantiating new wpdb instance.', 'db' ); $table_prefix = isset( $GLOBALS['table_prefix'] ) && is_string( $GLOBALS['table_prefix'] ) ? $GLOBALS['table_prefix'] : 'wp_'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $wpdb = new wpdb( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST ); $wpdb->set_prefix( $table_prefix ); } + + if ( ! isset( $GLOBALS['wpdb'] ) || ! ( $GLOBALS['wpdb'] instanceof wpdb ) ) { + WP_CLI::debug( sprintf( 'wpdb initialization failed. class_exists(wpdb): %s, isset($wpdb): %s', class_exists( 'wpdb' ) ? 'true' : 'false', isset( $GLOBALS['wpdb'] ) ? 'true' : 'false' ), 'db' ); + } } /** From 30dfd4fc0447f841bd9ef1e8d8b3ea10a1f1a9c9 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 22 Jul 2026 15:38:37 +0200 Subject: [PATCH 22/31] Support wp-db.php in WP 4.9 and older releases in maybe_load_wpdb --- src/DB_Command.php | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index adf5095b..f2cb7387 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -2473,41 +2473,38 @@ protected function maybe_load_wpdb() { define( 'WP_CONTENT_DIR', ABSPATH . 'wp-content' ); } + $wpdb_file = file_exists( ABSPATH . WPINC . '/class-wpdb.php' ) + ? ABSPATH . WPINC . '/class-wpdb.php' + : ABSPATH . WPINC . '/wp-db.php'; + // Load prerequisite WordPress files if not already loaded. - $prereqs = [ - 'load.php' => ABSPATH . WPINC . '/load.php', - 'compat.php' => ABSPATH . WPINC . '/compat.php', - 'plugin.php' => ABSPATH . WPINC . '/plugin.php', - 'class-wp-error.php' => ABSPATH . WPINC . '/class-wp-error.php', - 'class-wpdb.php' => ABSPATH . WPINC . '/class-wpdb.php', + $required_files = [ + ABSPATH . WPINC . '/load.php', + ABSPATH . WPINC . '/compat.php', + ABSPATH . WPINC . '/plugin.php', + ABSPATH . WPINC . '/class-wp-error.php', + $wpdb_file, ]; - foreach ( $prereqs as $name => $path ) { - if ( file_exists( $path ) ) { - WP_CLI::debug( "Loading WordPress core file: {$name}", 'db' ); - require_once $path; + foreach ( $required_files as $required_file ) { + if ( file_exists( $required_file ) ) { + require_once $required_file; } } // Load db.php drop-in if it exists (e.g., HyperDB or other custom drivers). $db_dropin_path = WP_CONTENT_DIR . '/db.php'; if ( file_exists( $db_dropin_path ) && ! $this->is_sqlite() ) { - WP_CLI::debug( "Loading db.php drop-in from {$db_dropin_path}", 'db' ); require_once $db_dropin_path; } // If $wpdb is still not set (e.g. no drop-in), create a new instance using the DB credentials from wp-config.php. if ( ! isset( $GLOBALS['wpdb'] ) && class_exists( 'wpdb' ) ) { - WP_CLI::debug( 'Instantiating new wpdb instance.', 'db' ); $table_prefix = isset( $GLOBALS['table_prefix'] ) && is_string( $GLOBALS['table_prefix'] ) ? $GLOBALS['table_prefix'] : 'wp_'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $wpdb = new wpdb( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST ); $wpdb->set_prefix( $table_prefix ); } - - if ( ! isset( $GLOBALS['wpdb'] ) || ! ( $GLOBALS['wpdb'] instanceof wpdb ) ) { - WP_CLI::debug( sprintf( 'wpdb initialization failed. class_exists(wpdb): %s, isset($wpdb): %s', class_exists( 'wpdb' ) ? 'true' : 'false', isset( $GLOBALS['wpdb'] ) ? 'true' : 'false' ), 'db' ); - } } /** From 66c44fa9c493561a481c7892b897f887b18b825f Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 28 Jul 2026 16:20:44 +0200 Subject: [PATCH 23/31] Improve client binary detection and wpdb fallback execution - Short-circuit is_mysql_binary_available() when binary path is empty. - Check binary availability before running commands without fallback (cli, export, check, optimize, repair) to avoid executing empty paths via /usr/bin/env. - Apply SQL mode compatibility statement before running queries and imports under wpdb fallback. - Pass custom dbuser, dbpass, dbname, and dbhost credentials to maybe_load_wpdb(). - Add support for multi-statement queries, -- space comment rules, # comments, and DELIMITER directives in split_sql_statements(). - Add error suppression to wpdb_query() and propagate COMMIT failures in wpdb_import(). - Update query and import docblocks to document wpdb fallback behavior. --- src/DB_Command.php | 243 ++++++++++++++++++++++++++++++++------------- 1 file changed, 175 insertions(+), 68 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index f2cb7387..def7eb7b 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -286,9 +286,14 @@ public function check( $_, $assoc_args ) { return; } + $check_cmd = Utils\get_sql_check_command(); + if ( '' === $check_cmd ) { + WP_CLI::error( 'mysqlcheck or mariadb-check binary is not available. Please install MySQL or MariaDB client tools.' ); + } + $command = sprintf( '/usr/bin/env %s%s %s', - Utils\get_sql_check_command(), + $check_cmd, $this->get_defaults_flag_string( $assoc_args ), '%s' ); @@ -347,9 +352,14 @@ public function optimize( $_, $assoc_args ) { return; } + $check_cmd = Utils\get_sql_check_command(); + if ( '' === $check_cmd ) { + WP_CLI::error( 'mysqlcheck or mariadb-check binary is not available. Please install MySQL or MariaDB client tools.' ); + } + $command = sprintf( '/usr/bin/env %s%s %s', - Utils\get_sql_check_command(), + $check_cmd, $this->get_defaults_flag_string( $assoc_args ), '%s' ); @@ -408,9 +418,14 @@ public function repair( $_, $assoc_args ) { return; } + $check_cmd = Utils\get_sql_check_command(); + if ( '' === $check_cmd ) { + WP_CLI::error( 'mysqlcheck or mariadb-check binary is not available. Please install MySQL or MariaDB client tools.' ); + } + $command = sprintf( '/usr/bin/env %s%s %s', - Utils\get_sql_check_command(), + $check_cmd, $this->get_defaults_flag_string( $assoc_args ), '%s' ); @@ -471,6 +486,10 @@ public function cli( $_, $assoc_args ) { return; } + if ( ! $this->is_mysql_binary_available() ) { + WP_CLI::error( 'MySQL/MariaDB client binary is not available. Please install MySQL or MariaDB client tools.' ); + } + $command = sprintf( '%s%s --no-auto-rehash', Utils\get_mysql_binary_path(), @@ -490,7 +509,9 @@ public function cli( $_, $assoc_args ) { * Executes a SQL query against the database. * * Executes an arbitrary SQL query using `DB_HOST`, `DB_NAME`, `DB_USER` - * and `DB_PASSWORD` database credentials specified in wp-config.php. + * and `DB_PASSWORD` database credentials specified in wp-config.php. + * If MySQL/MariaDB client binaries are not available, falls back to + * executing queries via WordPress's `wpdb`. * * Use the `--skip-column-names` MySQL argument to exclude the headers * from a SELECT query. Pipe the output to remove the ASCII table @@ -774,7 +795,12 @@ public function export( $args, $assoc_args ) { $assoc_args['result-file'] = $result_file; } - $mysqldump_binary = Utils\force_env_on_nix_systems( Utils\get_sql_dump_command() ); + $dump_cmd = Utils\get_sql_dump_command(); + if ( '' === $dump_cmd ) { + WP_CLI::error( 'mysqldump or mariadb-dump binary is not available. Please install MySQL or MariaDB client tools.' ); + } + + $mysqldump_binary = Utils\force_env_on_nix_systems( $dump_cmd ); $support_column_statistics = $this->command_supports_option( $mysqldump_binary, 'column-statistics' ); @@ -907,7 +933,8 @@ private function command_supports_option( $command, $option ) { * Runs SQL queries using `DB_HOST`, `DB_NAME`, `DB_USER` and * `DB_PASSWORD` database credentials specified in wp-config.php. This * does not create database by itself and only performs whatever tasks are - * defined in the SQL. + * defined in the SQL. If MySQL/MariaDB client binaries are not available, + * falls back to importing via WordPress's `wpdb`. * * ## OPTIONS * @@ -2444,8 +2471,13 @@ protected function is_mysql_binary_available() { static $available = null; if ( null === $available ) { - $result = \WP_CLI\Process::create( Utils\get_mysql_binary_path() . ' --version', null, null )->run(); - $available = 0 === $result->return_code; + $path = Utils\get_mysql_binary_path(); + if ( '' === $path ) { + $available = false; + } else { + $result = \WP_CLI\Process::create( $path . ' --version', null, null )->run(); + $available = 0 === $result->return_code; + } } return $available; @@ -2456,11 +2488,30 @@ protected function is_mysql_binary_available() { * * Loads the minimal required WordPress files to make $wpdb available, * including any db.php drop-in (e.g., HyperDB or other custom drivers). + * + * @param array $assoc_args Optional. Associative arguments containing db credentials. */ - protected function maybe_load_wpdb() { + protected function maybe_load_wpdb( $assoc_args = [] ) { global $wpdb; - if ( isset( $wpdb ) && $wpdb instanceof wpdb ) { + $default_user = defined( 'DB_USER' ) ? DB_USER : ''; + $default_pass = defined( 'DB_PASSWORD' ) ? DB_PASSWORD : ''; + $default_name = defined( 'DB_NAME' ) ? DB_NAME : ''; + $default_host = defined( 'DB_HOST' ) ? DB_HOST : ''; + + $db_user = Utils\get_flag_value( $assoc_args, 'dbuser', $default_user ); + $db_pass = Utils\get_flag_value( $assoc_args, 'dbpass', $default_pass ); + $db_name = Utils\get_flag_value( $assoc_args, 'dbname', $default_name ); + $db_host = Utils\get_flag_value( $assoc_args, 'dbhost', $default_host ); + + $has_custom_credentials = ( + ( isset( $assoc_args['dbuser'] ) && $default_user !== $assoc_args['dbuser'] ) || + ( isset( $assoc_args['dbpass'] ) && $default_pass !== $assoc_args['dbpass'] ) || + ( isset( $assoc_args['dbname'] ) && $default_name !== $assoc_args['dbname'] ) || + ( isset( $assoc_args['dbhost'] ) && $default_host !== $assoc_args['dbhost'] ) + ); + + if ( isset( $wpdb ) && $wpdb instanceof wpdb && ! $has_custom_credentials ) { return; } @@ -2494,15 +2545,15 @@ protected function maybe_load_wpdb() { // Load db.php drop-in if it exists (e.g., HyperDB or other custom drivers). $db_dropin_path = WP_CONTENT_DIR . '/db.php'; - if ( file_exists( $db_dropin_path ) && ! $this->is_sqlite() ) { + if ( file_exists( $db_dropin_path ) && ! $this->is_sqlite() && ! $has_custom_credentials ) { require_once $db_dropin_path; } - // If $wpdb is still not set (e.g. no drop-in), create a new instance using the DB credentials from wp-config.php. - if ( ! isset( $GLOBALS['wpdb'] ) && class_exists( 'wpdb' ) ) { + // If $wpdb is still not set or custom credentials were supplied, create a new instance using the DB credentials. + if ( ( ! isset( $GLOBALS['wpdb'] ) || ! ( $GLOBALS['wpdb'] instanceof wpdb ) || $has_custom_credentials ) && class_exists( 'wpdb' ) ) { $table_prefix = isset( $GLOBALS['table_prefix'] ) && is_string( $GLOBALS['table_prefix'] ) ? $GLOBALS['table_prefix'] : 'wp_'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $wpdb = new wpdb( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST ); + $wpdb = new wpdb( $db_user, $db_pass, $db_name, $db_host ); $wpdb->set_prefix( $table_prefix ); } } @@ -2517,61 +2568,92 @@ protected function maybe_load_wpdb() { * @param array $assoc_args Associative arguments. */ protected function wpdb_query( $query, $assoc_args = [] ) { - $this->maybe_load_wpdb(); + $this->maybe_load_wpdb( $assoc_args ); global $wpdb; if ( ! isset( $wpdb ) || ! ( $wpdb instanceof wpdb ) ) { WP_CLI::error( 'WordPress database (wpdb) is not available. Please install MySQL or MariaDB client tools.' ); } - $skip_column_names = Utils\get_flag_value( $assoc_args, 'skip-column-names', false ); - $is_row_modifying_query = (bool) preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE(?!\s*\()|LOAD DATA)\b/i', $query ); + $suppress = $wpdb->suppress_errors( true ); - if ( $is_row_modifying_query ) { - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $affected_rows = $wpdb->query( $query ); - if ( false === $affected_rows ) { - // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags - WP_CLI::error( 'Query failed: ' . strip_tags( $wpdb->last_error ) ); - } - WP_CLI::success( "Query succeeded. Rows affected: {$affected_rows}" ); - } else { + $sql_mode_compat = $this->get_sql_mode_compat_statement( $assoc_args ); + if ( '' !== $sql_mode_compat ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $results = $wpdb->get_results( $query, ARRAY_A ); + $wpdb->query( $sql_mode_compat ); + } - if ( $wpdb->last_error ) { - // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags - WP_CLI::error( 'Query failed: ' . strip_tags( $wpdb->last_error ) ); - } + $skip_column_names = Utils\get_flag_value( $assoc_args, 'skip-column-names', false ); + $statements = $this->split_sql_statements( $query ); - if ( empty( $results ) ) { - return; - } + if ( empty( $statements ) ) { + $wpdb->suppress_errors( $suppress ); + return; + } - $headers = array_keys( $results[0] ); - if ( ! $skip_column_names && ! empty( $headers ) ) { - WP_CLI::line( implode( "\t", $headers ) ); + foreach ( $statements as $statement ) { + $statement = trim( $statement ); + if ( '' === $statement ) { + continue; } - foreach ( $results as $row ) { - WP_CLI::line( - implode( - "\t", - array_map( - static function ( $v ) { - if ( null === $v ) { - return 'NULL'; - } - if ( is_scalar( $v ) ) { - return (string) $v; - } - return ''; - }, - array_values( $row ) + + $is_row_modifying_query = (bool) preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE(?!\s*\()|LOAD DATA|CREATE|DROP|ALTER|TRUNCATE)\b/i', $statement ); + + if ( $is_row_modifying_query ) { + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $affected_rows = $wpdb->query( $statement ); + if ( false === $affected_rows ) { + $error = $wpdb->last_error; + $wpdb->suppress_errors( $suppress ); + // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags + WP_CLI::error( 'Query failed: ' . strip_tags( $error ) ); + } + $is_dml = (bool) preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE(?!\s*\()|LOAD DATA)\b/i', $statement ); + if ( $is_dml ) { + WP_CLI::success( "Query succeeded. Rows affected: {$affected_rows}" ); + } + } else { + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $results = $wpdb->get_results( $statement, ARRAY_A ); + + if ( $wpdb->last_error ) { + $error = $wpdb->last_error; + $wpdb->suppress_errors( $suppress ); + // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags + WP_CLI::error( 'Query failed: ' . strip_tags( $error ) ); + } + + if ( empty( $results ) ) { + continue; + } + + $headers = array_keys( $results[0] ); + if ( ! $skip_column_names && ! empty( $headers ) ) { + WP_CLI::line( implode( "\t", $headers ) ); + } + foreach ( $results as $row ) { + WP_CLI::line( + implode( + "\t", + array_map( + static function ( $v ) { + if ( null === $v ) { + return 'NULL'; + } + if ( is_scalar( $v ) ) { + return (string) $v; + } + return ''; + }, + array_values( $row ) + ) ) - ) - ); + ); + } } } + + $wpdb->suppress_errors( $suppress ); } /** @@ -2583,7 +2665,7 @@ static function ( $v ) { * @param array $assoc_args Associative arguments. */ protected function wpdb_import( $sql_content, $assoc_args = [] ) { - $this->maybe_load_wpdb(); + $this->maybe_load_wpdb( $assoc_args ); global $wpdb; if ( ! isset( $wpdb ) || ! ( $wpdb instanceof wpdb ) ) { @@ -2592,6 +2674,12 @@ protected function wpdb_import( $sql_content, $assoc_args = [] ) { $suppress = $wpdb->suppress_errors( true ); + $sql_mode_compat = $this->get_sql_mode_compat_statement( $assoc_args ); + if ( '' !== $sql_mode_compat ) { + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( $sql_mode_compat ); + } + $skip_optimization = Utils\get_flag_value( $assoc_args, 'skip-optimization', false ); if ( ! $skip_optimization ) { @@ -2632,7 +2720,13 @@ protected function wpdb_import( $sql_content, $assoc_args = [] ) { if ( ! $skip_optimization ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $wpdb->query( 'COMMIT' ); + $commit_result = $wpdb->query( 'COMMIT' ); + if ( false === $commit_result ) { + $error = $wpdb->last_error; + $wpdb->suppress_errors( $suppress ); + // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags + WP_CLI::error( 'Import failed to commit: ' . strip_tags( $error ) ); + } // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( 'SET autocommit = 1' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared @@ -2647,8 +2741,8 @@ protected function wpdb_import( $sql_content, $assoc_args = [] ) { /** * Split a SQL string into individual statements. * - * Handles single-quoted strings, double-quoted strings, and comments - * so that semicolons inside them are not treated as statement delimiters. + * Handles single-quoted strings, double-quoted strings, comments, + * and DELIMITER directives so that semicolons inside them are not treated as statement delimiters. * * @param string $sql SQL string to split. * @return string[] Array of individual SQL statements. @@ -2661,6 +2755,7 @@ private function split_sql_statements( $sql ) { $in_comment = false; $in_line_comment = false; $in_conditional_comment = false; + $delimiter = ';'; $length = strlen( $sql ); for ( $i = 0; $i < $length; $i++ ) { @@ -2689,7 +2784,7 @@ private function split_sql_statements( $sql ) { ++$i; continue; } - // Fall through: treat content as regular SQL (handled below). + // Fall through: treat content as regular SQL. } if ( '/' === $char && '*' === $next && ! $in_single_quote && ! $in_double_quote ) { @@ -2698,16 +2793,12 @@ private function split_sql_statements( $sql ) { // MySQL conditional comment (/*!...*/): execute its SQL content. $in_conditional_comment = true; $i += 2; // skip past /*!; $i now points at ! - // Advance past optional version digits (e.g. "40101" in /*!40101 SET ... */). - // Loop checks the NEXT character so $i ends at the last digit. while ( $i + 1 < $length && ctype_digit( $sql[ $i + 1 ] ) ) { ++$i; } - // Skip one space following the version digits, if present. if ( $i + 1 < $length && ' ' === $sql[ $i + 1 ] ) { ++$i; } - // The for-loop's own ++$i then lands on the first SQL char. } else { $in_comment = true; ++$i; @@ -2716,6 +2807,14 @@ private function split_sql_statements( $sql ) { } if ( '-' === $char && '-' === $next && ! $in_single_quote && ! $in_double_quote ) { + $char_after = ( $i + 2 < $length ) ? $sql[ $i + 2 ] : "\n"; + if ( ' ' === $char_after || "\t" === $char_after || "\r" === $char_after || "\n" === $char_after ) { + $in_line_comment = true; + continue; + } + } + + if ( '#' === $char && ! $in_single_quote && ! $in_double_quote ) { $in_line_comment = true; continue; } @@ -2735,16 +2834,24 @@ private function split_sql_statements( $sql ) { $in_double_quote = ! $in_double_quote; } - if ( ';' === $char && ! $in_single_quote && ! $in_double_quote ) { - $statements[] = $current; - $current = ''; + $delim_len = strlen( $delimiter ); + if ( ! $in_single_quote && ! $in_double_quote && substr( $sql, $i, $delim_len ) === $delimiter ) { + $trimmed = trim( $current ); + if ( 0 === stripos( $trimmed, 'DELIMITER ' ) ) { + $delimiter = trim( substr( $trimmed, 10 ) ); + } elseif ( '' !== $trimmed ) { + $statements[] = $trimmed; + } + $current = ''; + $i += $delim_len - 1; } else { $current .= $char; } } - if ( '' !== trim( $current ) ) { - $statements[] = $current; + $trimmed = trim( $current ); + if ( '' !== $trimmed && 0 !== stripos( $trimmed, 'DELIMITER ' ) ) { + $statements[] = $trimmed; } return $statements; From 16d46e26a23b5ab347baaa428f7ddfab98937ac3 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 28 Jul 2026 16:20:56 +0200 Subject: [PATCH 24/31] Add acceptance tests for multi-statement queries, zero-date imports, and fallback behavior --- features/db-import.feature | 28 ++++++++++++++++++++++++++ features/db-query.feature | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/features/db-import.feature b/features/db-import.feature index 165ff4e0..1cfa71ec 100644 --- a/features/db-import.feature +++ b/features/db-import.feature @@ -97,6 +97,34 @@ Feature: Import a WordPress database MySQL/MariaDB binary not available, falling back to wpdb for import. """ + @require-mysql-or-mariadb + Scenario: Database import falls back to wpdb and handles zero-date column defaults + Given a WP install + And a fake-bin/mysql file: + """ + #!/bin/sh + exit 127 + """ + And a fake-bin/mariadb file: + """ + #!/bin/sh + exit 127 + """ + And a zero_date.sql file: + """ + CREATE TABLE IF NOT EXISTS wp_zero_date_test ( + id INT AUTO_INCREMENT PRIMARY KEY, + created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' + ); + """ + + When I run `chmod +x fake-bin/mysql fake-bin/mariadb` + And I try `env PATH={RUN_DIR}/fake-bin:$PATH wp db import zero_date.sql --debug` + Then STDOUT should be: + """ + Success: Imported from 'zero_date.sql'. + """ + # SQLite doesn't support the --dbuser flag. @require-mysql-or-mariadb Scenario: Import from database name path by default with passed-in dbuser/dbpass diff --git a/features/db-query.feature b/features/db-query.feature index 581eb2da..77586f12 100644 --- a/features/db-query.feature +++ b/features/db-query.feature @@ -181,3 +181,44 @@ Feature: Query the database with WordPress' MySQL config """ MySQL/MariaDB binary not available, falling back to wpdb. """ + + @require-mysql-or-mariadb + Scenario: Database querying falls back to wpdb for multi-statement queries when mysql binary is unavailable + Given a WP install + And a fake-bin/mysql file: + """ + #!/bin/sh + exit 127 + """ + And a fake-bin/mariadb file: + """ + #!/bin/sh + exit 127 + """ + + When I run `chmod +x fake-bin/mysql fake-bin/mariadb` + And I try `env PATH={RUN_DIR}/fake-bin:$PATH wp db query "SELECT 1 as num; SELECT 2 as num;" --debug` + Then STDOUT should contain: + """ + num + 1 + """ + And STDOUT should contain: + """ + num + 2 + """ + + Scenario: Database querying falls back to wpdb when PATH contains no binaries + Given a WP install + + When I try `env PATH=/empty_path wp db query "SELECT COUNT(ID) FROM wp_users;" --debug` + Then STDOUT should be: + """ + COUNT(ID) + 1 + """ + And STDERR should contain: + """ + MySQL/MariaDB binary not available, falling back to wpdb. + """ From cc9efd35e86ac8767e66691313dfa84dc6d8dde5 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 28 Jul 2026 16:49:06 +0200 Subject: [PATCH 25/31] Simplify is_mysql_binary_available() and remove invalid PATH scenario --- features/db-query.feature | 14 -------------- src/DB_Command.php | 14 +------------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/features/db-query.feature b/features/db-query.feature index 77586f12..d63f218a 100644 --- a/features/db-query.feature +++ b/features/db-query.feature @@ -208,17 +208,3 @@ Feature: Query the database with WordPress' MySQL config num 2 """ - - Scenario: Database querying falls back to wpdb when PATH contains no binaries - Given a WP install - - When I try `env PATH=/empty_path wp db query "SELECT COUNT(ID) FROM wp_users;" --debug` - Then STDOUT should be: - """ - COUNT(ID) - 1 - """ - And STDERR should contain: - """ - MySQL/MariaDB binary not available, falling back to wpdb. - """ diff --git a/src/DB_Command.php b/src/DB_Command.php index def7eb7b..805919b9 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -2468,19 +2468,7 @@ protected function apply_sql_mode_compat_init_command( &$mysql_args, $assoc_args * @return bool True if the binary is available, false otherwise. */ protected function is_mysql_binary_available() { - static $available = null; - - if ( null === $available ) { - $path = Utils\get_mysql_binary_path(); - if ( '' === $path ) { - $available = false; - } else { - $result = \WP_CLI\Process::create( $path . ' --version', null, null )->run(); - $available = 0 === $result->return_code; - } - } - - return $available; + return '' !== Utils\get_mysql_binary_path(); } /** From bd531f211e27374f4a7d2a0e74758a1d4b7f277a Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 28 Jul 2026 17:08:38 +0200 Subject: [PATCH 26/31] Fix binary probing and line-boundary DELIMITER parsing for wpdb fallback - Ensure is_mysql_binary_available() probes --version for non-empty binary paths so fake-bin test shims with non-zero exit codes correctly trigger wpdb fallback across all wp-cli dependency versions. - Recognize DELIMITER directives at line boundaries before normal statement accumulation in split_sql_statements(). - Add acceptance test for DELIMITER procedure imports under wpdb fallback. --- features/db-import.feature | 30 ++++++++++++++++++++++++++++++ src/DB_Command.php | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/features/db-import.feature b/features/db-import.feature index 02d3ed60..3447dd99 100644 --- a/features/db-import.feature +++ b/features/db-import.feature @@ -125,6 +125,36 @@ Feature: Import a WordPress database Success: Imported from 'zero_date.sql'. """ + @require-mysql-or-mariadb + Scenario: Database import falls back to wpdb and handles DELIMITER blocks with procedures + Given a WP install + And a fake-bin/mysql file: + """ + #!/bin/sh + exit 127 + """ + And a fake-bin/mariadb file: + """ + #!/bin/sh + exit 127 + """ + And a procedure.sql file: + """ + DELIMITER // + CREATE PROCEDURE wp_test_procedure() + BEGIN + SELECT 1; + END// + DELIMITER ; + """ + + When I run `chmod +x fake-bin/mysql fake-bin/mariadb` + And I try `env PATH={RUN_DIR}/fake-bin:$PATH wp db import procedure.sql --debug` + Then STDOUT should be: + """ + Success: Imported from 'procedure.sql'. + """ + # SQLite doesn't support the --dbuser flag. @require-mysql-or-mariadb Scenario: Import from database name path by default with passed-in dbuser/dbpass diff --git a/src/DB_Command.php b/src/DB_Command.php index 805919b9..42433543 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -2468,7 +2468,19 @@ protected function apply_sql_mode_compat_init_command( &$mysql_args, $assoc_args * @return bool True if the binary is available, false otherwise. */ protected function is_mysql_binary_available() { - return '' !== Utils\get_mysql_binary_path(); + static $available = null; + + if ( null === $available ) { + $path = Utils\get_mysql_binary_path(); + if ( '' === $path ) { + $available = false; + } else { + $result = \WP_CLI\Process::create( escapeshellarg( $path ) . ' --version', null, null )->run(); + $available = 0 === $result->return_code; + } + } + + return $available; } /** @@ -2750,6 +2762,22 @@ private function split_sql_statements( $sql ) { $char = $sql[ $i ]; $next = ( $i + 1 < $length ) ? $sql[ $i + 1 ] : ''; + // Check for DELIMITER directive at line boundary / statement start + if ( ! $in_single_quote && ! $in_double_quote && ! $in_comment && ! $in_line_comment && ! $in_conditional_comment ) { + $trimmed_current = trim( $current ); + if ( '' === $trimmed_current && 0 === stripos( substr( $sql, $i, 10 ), 'DELIMITER ' ) ) { + $line_end = strpos( $sql, "\n", $i ); + if ( false === $line_end ) { + $line_end = $length; + } + $delimiter_line = trim( substr( $sql, $i, $line_end - $i ) ); + $delimiter = trim( substr( $delimiter_line, 10 ) ); + $current = ''; + $i = $line_end; + continue; + } + } + if ( $in_line_comment ) { if ( "\n" === $char ) { $in_line_comment = false; @@ -2825,9 +2853,7 @@ private function split_sql_statements( $sql ) { $delim_len = strlen( $delimiter ); if ( ! $in_single_quote && ! $in_double_quote && substr( $sql, $i, $delim_len ) === $delimiter ) { $trimmed = trim( $current ); - if ( 0 === stripos( $trimmed, 'DELIMITER ' ) ) { - $delimiter = trim( substr( $trimmed, 10 ) ); - } elseif ( '' !== $trimmed ) { + if ( '' !== $trimmed ) { $statements[] = $trimmed; } $current = ''; @@ -2838,7 +2864,7 @@ private function split_sql_statements( $sql ) { } $trimmed = trim( $current ); - if ( '' !== $trimmed && 0 !== stripos( $trimmed, 'DELIMITER ' ) ) { + if ( '' !== $trimmed ) { $statements[] = $trimmed; } From 395876666349a759183fbb27b747a04622a1b476 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 28 Jul 2026 17:34:33 +0200 Subject: [PATCH 27/31] Include functions.php in maybe_load_wpdb() required files Ensure wp-includes/functions.php is loaded during wpdb bootstrap so helper functions like wp_debug_backtrace_summary() are available if wpdb::print_error() or get_caller() are invoked. --- src/DB_Command.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/DB_Command.php b/src/DB_Command.php index 42433543..652055c7 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -2532,6 +2532,7 @@ protected function maybe_load_wpdb( $assoc_args = [] ) { $required_files = [ ABSPATH . WPINC . '/load.php', ABSPATH . WPINC . '/compat.php', + ABSPATH . WPINC . '/functions.php', ABSPATH . WPINC . '/plugin.php', ABSPATH . WPINC . '/class-wp-error.php', $wpdb_file, From fbba09dfb740128d840f2ac5d5736f192d5cecad Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 28 Jul 2026 17:36:01 +0200 Subject: [PATCH 28/31] Update docblocks for all db subcommands with binary requirements and wpdb fallback details Document client binary requirements across create, drop, reset, check, optimize, repair, cli, and export subcommands, and clarify wpdb fallback behavior for query and import. --- src/DB_Command.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 652055c7..d201f550 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -69,7 +69,7 @@ class DB_Command extends WP_CLI_Command { * * Runs `CREATE_DATABASE` SQL statement using `DB_HOST`, `DB_NAME`, * `DB_USER` and `DB_PASSWORD` database credentials specified in - * wp-config.php. + * wp-config.php. Requires MySQL/MariaDB client binaries to be available. * * ## OPTIONS * @@ -105,7 +105,7 @@ public function create( $_, $assoc_args ) { * * Runs `DROP_DATABASE` SQL statement using `DB_HOST`, `DB_NAME`, * `DB_USER` and `DB_PASSWORD` database credentials specified in - * wp-config.php. + * wp-config.php. Requires MySQL/MariaDB client binaries to be available. * * ## OPTIONS * @@ -148,7 +148,7 @@ public function drop( $_, $assoc_args ) { * * Runs `DROP_DATABASE` and `CREATE_DATABASE` SQL statements using * `DB_HOST`, `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials - * specified in wp-config.php. + * specified in wp-config.php. Requires MySQL/MariaDB client binaries to be available. * * ## OPTIONS * @@ -251,7 +251,7 @@ public function clean( $_, $assoc_args ) { * * Runs `mysqlcheck` utility with `--check` using `DB_HOST`, * `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials - * specified in wp-config.php. + * specified in wp-config.php. Requires `mysqlcheck` or `mariadb-check` client binary. * * [See docs](http://dev.mysql.com/doc/refman/5.7/en/check-table.html) * for more details on the `CHECK TABLE` statement. @@ -320,7 +320,7 @@ public function check( $_, $assoc_args ) { * * Runs `mysqlcheck` utility with `--optimize=true` using `DB_HOST`, * `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials - * specified in wp-config.php. + * specified in wp-config.php. Requires `mysqlcheck` or `mariadb-check` client binary. * * [See docs](http://dev.mysql.com/doc/refman/5.7/en/optimize-table.html) * for more details on the `OPTIMIZE TABLE` statement. @@ -386,7 +386,7 @@ public function optimize( $_, $assoc_args ) { * * Runs `mysqlcheck` utility with `--repair=true` using `DB_HOST`, * `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials - * specified in wp-config.php. + * specified in wp-config.php. Requires `mysqlcheck` or `mariadb-check` client binary. * * [See docs](http://dev.mysql.com/doc/refman/5.7/en/repair-table.html) for * more details on the `REPAIR TABLE` statement. @@ -448,7 +448,8 @@ public function repair( $_, $assoc_args ) { } /** - * Opens a MySQL console using credentials from wp-config.php + * Opens a MySQL console using credentials from wp-config.php. + * Requires `mysql` or `mariadb` client binary to be available. * * ## OPTIONS * @@ -686,6 +687,7 @@ public function query( $args, $assoc_args ) { * * Runs `mysqldump` utility using `DB_HOST`, `DB_NAME`, `DB_USER` and * `DB_PASSWORD` database credentials specified in wp-config.php. Accepts any valid [`mysqldump` flags](https://dev.mysql.com/doc/en/mysqldump.html#mysqldump-option-summary). + * Requires `mysqldump` or `mariadb-dump` client binary to be available. * * ## OPTIONS * From 4d1ce7a7cb8c7b28fc45e78cc57692d947c7ab70 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 28 Jul 2026 17:38:55 +0200 Subject: [PATCH 29/31] Update readme --- README.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 38a83303..dfed369e 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ wp db create [--dbuser=] [--dbpass=] [--defaults] Runs `CREATE_DATABASE` SQL statement using `DB_HOST`, `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials specified in -wp-config.php. +wp-config.php. Requires MySQL/MariaDB client binaries to be available. **OPTIONS** @@ -111,7 +111,7 @@ wp db drop [--dbuser=] [--dbpass=] [--yes] [--defaults] Runs `DROP_DATABASE` SQL statement using `DB_HOST`, `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials specified in -wp-config.php. +wp-config.php. Requires MySQL/MariaDB client binaries to be available. **OPTIONS** @@ -144,7 +144,7 @@ wp db reset [--dbuser=] [--dbpass=] [--yes] [--defaults] Runs `DROP_DATABASE` and `CREATE_DATABASE` SQL statements using `DB_HOST`, `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials -specified in wp-config.php. +specified in wp-config.php. Requires MySQL/MariaDB client binaries to be available. **OPTIONS** @@ -177,7 +177,7 @@ wp db check [--dbuser=] [--dbpass=] [--=] [--default Runs `mysqlcheck` utility with `--check` using `DB_HOST`, `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials -specified in wp-config.php. +specified in wp-config.php. Requires `mysqlcheck` or `mariadb-check` client binary. [See docs](http://dev.mysql.com/doc/refman/5.7/en/check-table.html) for more details on the `CHECK TABLE` statement. @@ -216,7 +216,7 @@ wp db optimize [--dbuser=] [--dbpass=] [--=] [--defa Runs `mysqlcheck` utility with `--optimize=true` using `DB_HOST`, `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials -specified in wp-config.php. +specified in wp-config.php. Requires `mysqlcheck` or `mariadb-check` client binary. [See docs](http://dev.mysql.com/doc/refman/5.7/en/optimize-table.html) for more details on the `OPTIMIZE TABLE` statement. @@ -269,7 +269,7 @@ wp db repair [--dbuser=] [--dbpass=] [--=] [--defaul Runs `mysqlcheck` utility with `--repair=true` using `DB_HOST`, `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials -specified in wp-config.php. +specified in wp-config.php. Requires `mysqlcheck` or `mariadb-check` client binary. [See docs](http://dev.mysql.com/doc/refman/5.7/en/repair-table.html) for more details on the `REPAIR TABLE` statement. @@ -297,7 +297,7 @@ more details on the `REPAIR TABLE` statement. ### wp db cli -Opens a MySQL console using credentials from wp-config.php +Opens a MySQL console using credentials from wp-config.php. ~~~ wp db cli [--database=] [--default-character-set=] [--dbuser=] [--dbpass=] [--=] [--defaults] @@ -305,6 +305,8 @@ wp db cli [--database=] [--default-character-set=] [--d **Alias:** `connect` +Requires `mysql` or `mariadb` client binary to be available. + **OPTIONS** [--database=] @@ -342,7 +344,9 @@ wp db query [] [--dbuser=] [--dbpass=] [--=] [- ~~~ Executes an arbitrary SQL query using `DB_HOST`, `DB_NAME`, `DB_USER` - and `DB_PASSWORD` database credentials specified in wp-config.php. +and `DB_PASSWORD` database credentials specified in wp-config.php. +If MySQL/MariaDB client binaries are not available, falls back to +executing queries via WordPress's `wpdb`. Use the `--skip-column-names` MySQL argument to exclude the headers from a SELECT query. Pipe the output to remove the ASCII table @@ -445,6 +449,7 @@ wp db export [] [--dbuser=] [--dbpass=] [--=] Runs `mysqldump` utility using `DB_HOST`, `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials specified in wp-config.php. Accepts any valid [`mysqldump` flags](https://dev.mysql.com/doc/en/mysqldump.html#mysqldump-option-summary). +Requires `mysqldump` or `mariadb-dump` client binary to be available. **OPTIONS** @@ -536,7 +541,8 @@ wp db import [] [--dbuser=] [--dbpass=] [--=] Runs SQL queries using `DB_HOST`, `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials specified in wp-config.php. This does not create database by itself and only performs whatever tasks are -defined in the SQL. +defined in the SQL. If MySQL/MariaDB client binaries are not available, +falls back to importing via WordPress's `wpdb`. **OPTIONS** From f0eb585aad6f49da1d1243c16a21f55905da1b04 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 28 Jul 2026 17:39:21 +0200 Subject: [PATCH 30/31] Prevent infinite loop on malformed empty DELIMITER lines Ensure split_sql_statements() does not accept empty delimiter tokens on malformed DELIMITER lines, preventing zero-length delimiter matching and infinite loops. --- features/db-import.feature | 26 ++++++++++++++++++++++++++ src/DB_Command.php | 24 +++++++++++++++--------- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/features/db-import.feature b/features/db-import.feature index 3447dd99..e1b27200 100644 --- a/features/db-import.feature +++ b/features/db-import.feature @@ -155,6 +155,32 @@ Feature: Import a WordPress database Success: Imported from 'procedure.sql'. """ + @require-mysql-or-mariadb + Scenario: Database import falls back to wpdb and handles malformed empty DELIMITER lines + Given a WP install + And a fake-bin/mysql file: + """ + #!/bin/sh + exit 127 + """ + And a fake-bin/mariadb file: + """ + #!/bin/sh + exit 127 + """ + And a malformed_delimiter.sql file: + """ + DELIMITER + SELECT 1; + """ + + When I run `chmod +x fake-bin/mysql fake-bin/mariadb` + And I try `env PATH={RUN_DIR}/fake-bin:$PATH wp db import malformed_delimiter.sql --debug` + Then STDOUT should be: + """ + Success: Imported from 'malformed_delimiter.sql'. + """ + # SQLite doesn't support the --dbuser flag. @require-mysql-or-mariadb Scenario: Import from database name path by default with passed-in dbuser/dbpass diff --git a/src/DB_Command.php b/src/DB_Command.php index d201f550..86f10b68 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -2768,16 +2768,22 @@ private function split_sql_statements( $sql ) { // Check for DELIMITER directive at line boundary / statement start if ( ! $in_single_quote && ! $in_double_quote && ! $in_comment && ! $in_line_comment && ! $in_conditional_comment ) { $trimmed_current = trim( $current ); - if ( '' === $trimmed_current && 0 === stripos( substr( $sql, $i, 10 ), 'DELIMITER ' ) ) { - $line_end = strpos( $sql, "\n", $i ); - if ( false === $line_end ) { - $line_end = $length; + if ( '' === $trimmed_current && 0 === stripos( substr( $sql, $i, 9 ), 'DELIMITER' ) ) { + $char_after = ( $i + 9 < $length ) ? $sql[ $i + 9 ] : "\n"; + if ( ' ' === $char_after || "\t" === $char_after || "\r" === $char_after || "\n" === $char_after ) { + $line_end = strpos( $sql, "\n", $i ); + if ( false === $line_end ) { + $line_end = $length; + } + $delimiter_line = trim( substr( $sql, $i, $line_end - $i ) ); + $new_delimiter = trim( substr( $delimiter_line, 9 ) ); + if ( '' !== $new_delimiter ) { + $delimiter = $new_delimiter; + } + $current = ''; + $i = $line_end; + continue; } - $delimiter_line = trim( substr( $sql, $i, $line_end - $i ) ); - $delimiter = trim( substr( $delimiter_line, 10 ) ); - $current = ''; - $i = $line_end; - continue; } } From ece1b7305955cae560828989758300cc63a9bd68 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 28 Jul 2026 17:47:46 +0200 Subject: [PATCH 31/31] Translate --host, --user, --password, and --database options in maybe_load_wpdb() Ensure all standard MySQL connection flags (user, password/pass, database, host) as well as dbuser/dbpass/dbname/dbhost are mapped to the fallback wpdb instance. --- src/DB_Command.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 86f10b68..57269041 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -2501,16 +2501,16 @@ protected function maybe_load_wpdb( $assoc_args = [] ) { $default_name = defined( 'DB_NAME' ) ? DB_NAME : ''; $default_host = defined( 'DB_HOST' ) ? DB_HOST : ''; - $db_user = Utils\get_flag_value( $assoc_args, 'dbuser', $default_user ); - $db_pass = Utils\get_flag_value( $assoc_args, 'dbpass', $default_pass ); - $db_name = Utils\get_flag_value( $assoc_args, 'dbname', $default_name ); - $db_host = Utils\get_flag_value( $assoc_args, 'dbhost', $default_host ); + $db_user = Utils\get_flag_value( $assoc_args, 'dbuser', Utils\get_flag_value( $assoc_args, 'user', $default_user ) ); + $db_pass = Utils\get_flag_value( $assoc_args, 'dbpass', Utils\get_flag_value( $assoc_args, 'password', Utils\get_flag_value( $assoc_args, 'pass', $default_pass ) ) ); + $db_name = Utils\get_flag_value( $assoc_args, 'dbname', Utils\get_flag_value( $assoc_args, 'database', $default_name ) ); + $db_host = Utils\get_flag_value( $assoc_args, 'dbhost', Utils\get_flag_value( $assoc_args, 'host', $default_host ) ); $has_custom_credentials = ( - ( isset( $assoc_args['dbuser'] ) && $default_user !== $assoc_args['dbuser'] ) || - ( isset( $assoc_args['dbpass'] ) && $default_pass !== $assoc_args['dbpass'] ) || - ( isset( $assoc_args['dbname'] ) && $default_name !== $assoc_args['dbname'] ) || - ( isset( $assoc_args['dbhost'] ) && $default_host !== $assoc_args['dbhost'] ) + ( $default_user !== $db_user ) || + ( $default_pass !== $db_pass ) || + ( $default_name !== $db_name ) || + ( $default_host !== $db_host ) ); if ( isset( $wpdb ) && $wpdb instanceof wpdb && ! $has_custom_credentials ) {