diff --git a/composer.json b/composer.json index e575f01f..34fbdb20 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,8 @@ "db search", "db tables", "db size", - "db columns" + "db columns", + "db status" ] }, "autoload": { diff --git a/features/db-status.feature b/features/db-status.feature new file mode 100644 index 00000000..138bf726 --- /dev/null +++ b/features/db-status.feature @@ -0,0 +1,89 @@ +Feature: Display database status overview + + Scenario: Display database status for a WordPress install + Given a WP install + + When I run `wp db status` + Then STDOUT should contain: + """ + Database Name: wp_cli_test + """ + And STDOUT should match /^Tables:\s+\d+$/m + And STDOUT should match /^Total Size:\s+[\d.]+ \wB$/m + And STDOUT should contain: + """ + Prefix: wp_ + """ + And STDOUT should match /^Engine:\s+\w+$/m + And STDOUT should match /^Charset:\s+\w+$/m + And STDOUT should match /^Collation:\s+\w+$/m + And STDOUT should contain: + """ + Check Status: OK + """ + + Scenario: Run db status with MySQL defaults to check the database + Given a WP install + + When I run `wp db status --defaults` + Then STDOUT should contain: + """ + Database Name: + """ + And STDOUT should contain: + """ + Check Status: OK + """ + + Scenario: Run db status with --no-defaults to check the database + Given a WP install + + When I run `wp db status --no-defaults` + Then STDOUT should contain: + """ + Database Name: + """ + And STDOUT should contain: + """ + Check Status: OK + """ + + Scenario: Run db status with passed-in options + Given a WP install + + When I run `wp db status --dbuser=wp_cli_test` + Then STDOUT should contain: + """ + Database Name: + """ + And STDOUT should contain: + """ + Check Status: OK + """ + + When I run `wp db status --dbpass=password1` + Then STDOUT should contain: + """ + Database Name: + """ + And STDOUT should contain: + """ + Check Status: OK + """ + + When I run `wp db status --dbuser=wp_cli_test --dbpass=password1` + Then STDOUT should contain: + """ + Database Name: + """ + And STDOUT should contain: + """ + Check Status: OK + """ + + When I try `wp db status --dbuser=no_such_user` + Then the return code should not be 0 + + When I try `wp db status --dbpass=no_such_pass` + Then the return code should not be 0 + diff --git a/src/DB_Command.php b/src/DB_Command.php index 7409935c..22fc425c 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1271,6 +1271,174 @@ public function prefix() { WP_CLI::log( $wpdb->prefix ); } + /** + * Displays a quick database status overview. + * + * Shows key database information including name, table count, size, + * prefix, engine, charset, collation, and health check status. This + * command is useful for getting a quick snapshot of database health + * without needing to run multiple separate commands. + * + * This command works even when WordPress is not fully installed, + * providing minimal information. If WordPress is installed, additional + * details are included. + * + * ## OPTIONS + * + * [--dbuser=] + * : Username to pass to mysql. Defaults to DB_USER. + * + * [--dbpass=] + * : Password to pass to mysql. Defaults to DB_PASSWORD. + * + * [--defaults] + * : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration. + * + * ## EXAMPLES + * + * $ wp db status + * Database Name: wp_cli_test + * Tables: 54 + * Total Size: 312 KB + * Prefix: wp_ + * Engine: InnoDB + * Charset: utf8mb4 + * Collation: utf8mb4_unicode_ci + * Check Status: OK + */ + public function status( $_, $assoc_args ) { + global $table_prefix; + + // Get database name from constants (available after wp-config loads). + $db_name = DB_NAME; + + // Get table prefix from wp-config.php (available after wp-config loads). + $prefix = isset( $table_prefix ) ? $table_prefix : 'wp_'; + + // Escape prefix for SQL: first addslashes, then escape LIKE special characters. + $escaped_prefix = str_replace( [ '%', '_' ], [ '\\%', '\\_' ], addslashes( $prefix ) ); + + // Prepare command for executing queries. + $command = sprintf( + '/usr/bin/env %s%s --no-auto-rehash --batch --skip-column-names', + $this->get_mysql_command(), + $this->get_defaults_flag_string( $assoc_args ) + ); + + // Get table count using raw SQL query. + $table_count_query = sprintf( + "SELECT COUNT(*) FROM information_schema.TABLES WHERE table_schema = '%s' AND table_name LIKE '%s%%'", + addslashes( DB_NAME ), + $escaped_prefix + ); + list( $stdout, $stderr, $exit_code ) = self::run( + $command, + array_merge( [ 'execute' => $table_count_query ], $assoc_args ), + false + ); + $table_count = (int) trim( $stdout ); + + // Get total database size using raw SQL query. + $db_size_query = sprintf( + "SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema = '%s'", + addslashes( DB_NAME ) + ); + list( $stdout, $stderr, $exit_code ) = self::run( + $command, + array_merge( [ 'execute' => $db_size_query ], $assoc_args ), + false + ); + $db_size_bytes = trim( $stdout ); + + // Format size to human-readable. + if ( ! empty( $db_size_bytes ) && $db_size_bytes > 0 ) { + $size_key = floor( log( (float) $db_size_bytes ) / log( 1000 ) ); + $sizes = [ 'B', 'KB', 'MB', 'GB', 'TB' ]; + $size_format = $sizes[ (int) $size_key ] ?? $sizes[0]; + $divisor = pow( 1000, $size_key ); + $db_size = round( (int) $db_size_bytes / $divisor, 2 ) . ' ' . $size_format; + } else { + $db_size = '0 B'; + } + + // Get engine, charset, and collation from information_schema across all tables with the prefix. + $table_info_query = sprintf( + 'SELECT ' + . 'COUNT(DISTINCT ENGINE) AS engine_count, ' + . 'MIN(ENGINE) AS engine, ' + . 'COUNT(DISTINCT CCSA.character_set_name) AS charset_count, ' + . 'MIN(CCSA.character_set_name) AS charset, ' + . 'COUNT(DISTINCT TABLE_COLLATION) AS collation_count, ' + . 'MIN(TABLE_COLLATION) AS collation ' + . 'FROM information_schema.TABLES T ' + . 'LEFT JOIN information_schema.COLLATION_CHARACTER_SET_APPLICABILITY CCSA ' + . 'ON CCSA.collation_name = T.table_collation ' + . "WHERE T.table_schema = '%s' " + . "AND T.table_name LIKE '%s%%'", + addslashes( DB_NAME ), + $escaped_prefix + ); + list( $stdout, $stderr, $exit_code ) = self::run( + $command, + array_merge( [ 'execute' => $table_info_query ], $assoc_args ), + false + ); + + // Parse the tab-separated output. + $table_info_parts = explode( "\t", trim( $stdout ) ); + + if ( count( $table_info_parts ) >= 6 ) { + $engine_count = (int) $table_info_parts[0]; + $engine_value = $table_info_parts[1]; + $charset_count = (int) $table_info_parts[2]; + $charset_value = $table_info_parts[3]; + $collation_count = (int) $table_info_parts[4]; + $collation_value = $table_info_parts[5]; + + $engine = $engine_count > 1 ? 'Mixed' : ( ! empty( $engine_value ) && 'NULL' !== $engine_value ? $engine_value : 'N/A' ); + $charset = $charset_count > 1 ? 'Mixed' : ( ! empty( $charset_value ) && 'NULL' !== $charset_value ? $charset_value : 'N/A' ); + $collation = $collation_count > 1 ? 'Mixed' : ( ! empty( $collation_value ) && 'NULL' !== $collation_value ? $collation_value : 'N/A' ); + } else { + $engine = 'N/A'; + $charset = 'N/A'; + $collation = 'N/A'; + } + + // Run database check silently to get status. + if ( $table_count > 0 ) { + $check_command = sprintf( + '/usr/bin/env %s%s %s', + Utils\get_sql_check_command(), + $this->get_defaults_flag_string( $assoc_args ), + '%s' + ); + list( $stdout, $stderr, $exit_code ) = self::run( + Utils\esc_cmd( $check_command, DB_NAME ), + array_merge( [ 'check' => true ], $assoc_args ), + false + ); + $check_status = ( 0 === $exit_code ) ? 'OK' : 'Error'; + } else { + // No tables to check; mark status as not applicable. + $check_status = 'N/A'; + } + + $status_items = [ + 'Database Name' => $db_name, + 'Tables' => $table_count, + 'Total Size' => $db_size, + 'Prefix' => $prefix, + 'Engine' => $engine, + 'Charset' => $charset, + 'Collation' => $collation, + 'Check Status' => $check_status, + ]; + + foreach ( $status_items as $label => $value ) { + WP_CLI::log( sprintf( '%-18s %s', $label . ':', $value ) ); + } + } + /** * Finds a string in the database. *