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 1/3] 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 2/3] 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 3/3] 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'] ); + } } }