Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions features/db-query.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Comment thread
swissspidy marked this conversation as resolved.
Outdated
Then STDOUT should be:
"""
COUNT(ID)
1
"""
And STDERR should contain:
"""
MySQL/MariaDB binary not available, falling back to wpdb.
"""
300 changes: 300 additions & 0 deletions src/DB_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -910,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 ],
Expand Down Expand Up @@ -1900,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;

Expand Down Expand Up @@ -2348,4 +2404,248 @@ 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 ' . escapeshellarg( $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 . '/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 ( $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 );
if ( isset( $GLOBALS['table_prefix'] ) && is_string( $GLOBALS['table_prefix'] ) ) {
$wpdb->set_prefix( $GLOBALS['table_prefix'] );
}
}
}

/**
* 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 );
}
}

/**
* 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;
}
}
Loading