From 733e172e0226a01ee2345f52c98a588723cf5e5a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:18:51 +0000 Subject: [PATCH 01/20] Initial plan From cbcf1583047734ed9865809cf3951164563209c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:30:58 +0000 Subject: [PATCH 02/20] Add SQLite support trait and integrate into DB_Command Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/DB_Command.php | 134 ++++++++++-- src/DB_Command_SQLite.php | 449 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 567 insertions(+), 16 deletions(-) create mode 100644 src/DB_Command_SQLite.php diff --git a/src/DB_Command.php b/src/DB_Command.php index 06781cee..347e7057 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -3,6 +3,8 @@ use WP_CLI\Formatter; use WP_CLI\Utils; +require_once __DIR__ . '/DB_Command_SQLite.php'; + /** * Performs basic database operations using credentials stored in wp-config.php. * @@ -27,6 +29,8 @@ */ class DB_Command extends WP_CLI_Command { + use DB_Command_SQLite; + /** * Legacy UTF-8 encoding for MySQL. * @@ -82,6 +86,12 @@ class DB_Command extends WP_CLI_Command { * Success: Database created. */ public function create( $_, $assoc_args ) { + $this->maybe_load_sqlite_dropin(); + + if ( $this->is_sqlite() ) { + $this->sqlite_create( $assoc_args ); + return; + } $this->run_query( self::get_create_query(), $assoc_args ); @@ -115,6 +125,15 @@ public function create( $_, $assoc_args ) { * Success: Database dropped. */ public function drop( $_, $assoc_args ) { + $this->maybe_load_sqlite_dropin(); + + if ( $this->is_sqlite() ) { + $db_path = $this->get_sqlite_db_path(); + WP_CLI::confirm( "Are you sure you want to drop the SQLite database at '{$db_path}'?", $assoc_args ); + $this->sqlite_drop( $assoc_args ); + return; + } + WP_CLI::confirm( "Are you sure you want to drop the '" . DB_NAME . "' database?", $assoc_args ); $this->run_query( sprintf( 'DROP DATABASE `%s`', DB_NAME ), $assoc_args ); @@ -149,6 +168,15 @@ public function drop( $_, $assoc_args ) { * Success: Database reset. */ public function reset( $_, $assoc_args ) { + $this->maybe_load_sqlite_dropin(); + + if ( $this->is_sqlite() ) { + $db_path = $this->get_sqlite_db_path(); + WP_CLI::confirm( "Are you sure you want to reset the SQLite database at '{$db_path}'?", $assoc_args ); + $this->sqlite_reset( $assoc_args ); + return; + } + WP_CLI::confirm( "Are you sure you want to reset the '" . DB_NAME . "' database?", $assoc_args ); $this->run_query( sprintf( 'DROP DATABASE IF EXISTS `%s`', DB_NAME ), $assoc_args ); @@ -249,6 +277,12 @@ public function clean( $_, $assoc_args ) { * Success: Database checked. */ public function check( $_, $assoc_args ) { + $this->maybe_load_sqlite_dropin(); + + if ( $this->is_sqlite() ) { + WP_CLI::warning( 'Database check is not supported for SQLite databases.' ); + return; + } $command = sprintf( '/usr/bin/env %s%s %s', @@ -298,6 +332,13 @@ public function check( $_, $assoc_args ) { * Success: Database optimized. */ public function optimize( $_, $assoc_args ) { + $this->maybe_load_sqlite_dropin(); + + if ( $this->is_sqlite() ) { + WP_CLI::warning( 'Database optimization is not supported for SQLite databases. SQLite automatically optimizes on VACUUM.' ); + return; + } + $command = sprintf( '/usr/bin/env %s%s %s', Utils\get_sql_check_command(), @@ -346,6 +387,13 @@ public function optimize( $_, $assoc_args ) { * Success: Database repaired. */ public function repair( $_, $assoc_args ) { + $this->maybe_load_sqlite_dropin(); + + if ( $this->is_sqlite() ) { + WP_CLI::warning( 'Database repair is not supported for SQLite databases.' ); + return; + } + $command = sprintf( '/usr/bin/env %s%s %s', Utils\get_sql_check_command(), @@ -396,6 +444,12 @@ public function repair( $_, $assoc_args ) { * @alias connect */ public function cli( $_, $assoc_args ) { + $this->maybe_load_sqlite_dropin(); + + if ( $this->is_sqlite() ) { + WP_CLI::warning( 'Interactive console (cli) is not supported for SQLite databases. Use `wp db query` instead.' ); + return; + } $command = sprintf( '/usr/bin/env %s%s --no-auto-rehash', @@ -499,6 +553,24 @@ public function cli( $_, $assoc_args ) { * +---------+-----------------------+ */ public function query( $args, $assoc_args ) { + $this->maybe_load_sqlite_dropin(); + + if ( $this->is_sqlite() ) { + // 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.' ); + } + + $this->sqlite_query( $query, $assoc_args ); + return; + } $command = sprintf( '/usr/bin/env %s%s --no-auto-rehash', @@ -629,14 +701,23 @@ public function query( $args, $assoc_args ) { * @alias dump */ public function export( $args, $assoc_args ) { + $this->maybe_load_sqlite_dropin(); + if ( ! empty( $args[0] ) ) { $result_file = $args[0]; } else { // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- WordPress is not loaded. $hash = substr( md5( (string) mt_rand() ), 0, 7 ); - $result_file = sprintf( '%s-%s-%s.sql', DB_NAME, date( 'Y-m-d' ), $hash ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + $db_name = $this->is_sqlite() ? 'sqlite-db' : DB_NAME; + $result_file = sprintf( '%s-%s-%s.sql', $db_name, date( 'Y-m-d' ), $hash ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + + } + if ( $this->is_sqlite() ) { + $this->sqlite_export( $result_file, $assoc_args ); + return; } + $stdout = ( '-' === $result_file ); $porcelain = Utils\get_flag_value( $assoc_args, 'porcelain' ); @@ -798,10 +879,17 @@ private function get_posts_table_charset( $assoc_args ) { * Success: Imported from 'wordpress_dbase.sql'. */ public function import( $args, $assoc_args ) { + $this->maybe_load_sqlite_dropin(); + if ( ! empty( $args[0] ) ) { $result_file = $args[0]; } else { - $result_file = sprintf( '%s.sql', DB_NAME ); + $result_file = $this->is_sqlite() ? 'sqlite-db.sql' : sprintf( '%s.sql', DB_NAME ); + } + + if ( $this->is_sqlite() ) { + $this->sqlite_import( $result_file, $assoc_args ); + return; } // Process options to MySQL. @@ -1080,19 +1168,27 @@ public function size( $args, $assoc_args ) { $default_unit = ( empty( $size_format ) && ! $human_readable ) ? ' B' : ''; + $is_sqlite = $this->is_sqlite(); + if ( $tables || $all_tables || $all_tables_with_prefix ) { // Add all of the table sizes. foreach ( Utils\wp_get_table_names( $args, $assoc_args ) as $table_name ) { // Get the table size. - $table_bytes = $wpdb->get_var( - $wpdb->prepare( - 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES where table_schema = %s and Table_Name = %s GROUP BY Table_Name LIMIT 1', - DB_NAME, - $table_name - ) - ); + if ( $is_sqlite ) { + // For SQLite, we cannot get individual table sizes easily. + // Just report 0 as a placeholder. + $table_bytes = 0; + } else { + $table_bytes = $wpdb->get_var( + $wpdb->prepare( + 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES where table_schema = %s and Table_Name = %s GROUP BY Table_Name LIMIT 1', + DB_NAME, + $table_name + ) + ); + } // Add the table size to the list. $rows[] = [ @@ -1104,16 +1200,22 @@ public function size( $args, $assoc_args ) { } else { // Get the database size. - $db_bytes = $wpdb->get_var( - $wpdb->prepare( - 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES where table_schema = %s GROUP BY table_schema;', - DB_NAME - ) - ); + if ( $is_sqlite ) { + $db_bytes = $this->sqlite_size(); + $db_name = basename( $this->get_sqlite_db_path() ); + } else { + $db_bytes = $wpdb->get_var( + $wpdb->prepare( + 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES where table_schema = %s GROUP BY table_schema;', + DB_NAME + ) + ); + $db_name = DB_NAME; + } // Add the database size to the list. $rows[] = [ - 'Name' => DB_NAME, + 'Name' => $db_name, 'Size' => strtoupper( $db_bytes ) . $default_unit, 'Bytes' => strtoupper( $db_bytes ), ]; diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php new file mode 100644 index 00000000..2f080d8a --- /dev/null +++ b/src/DB_Command_SQLite.php @@ -0,0 +1,449 @@ +get_sqlite_db_path(); + + if ( ! $db_path ) { + return false; + } + + try { + $pdo = new PDO( 'sqlite:' . $db_path ); + $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); + return $pdo; + } catch ( PDOException $e ) { + WP_CLI::debug( 'SQLite PDO connection failed: ' . $e->getMessage(), 'db' ); + return false; + } + } + + /** + * Create SQLite database. + * + * @param array $assoc_args Associative arguments (unused for SQLite). + */ + protected function sqlite_create( $assoc_args ) { + $db_path = $this->get_sqlite_db_path(); + $db_dir = dirname( $db_path ); + + // Create directory if it doesn't exist. + if ( ! is_dir( $db_dir ) ) { + if ( ! mkdir( $db_dir, 0755, true ) ) { + WP_CLI::error( "Could not create directory: {$db_dir}" ); + } + } + + // Check if database already exists. + if ( file_exists( $db_path ) ) { + WP_CLI::error( 'Database already exists.' ); + } + + // Create the SQLite database file. + try { + $pdo = new PDO( 'sqlite:' . $db_path ); + $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); + // Execute a simple query to initialize the database. + $pdo->exec( 'CREATE TABLE IF NOT EXISTS _wpcli_test (id INTEGER)' ); + $pdo->exec( 'DROP TABLE _wpcli_test' ); + } catch ( PDOException $e ) { + WP_CLI::error( 'Could not create SQLite database: ' . $e->getMessage() ); + } + + WP_CLI::success( 'Database created.' ); + } + + /** + * Drop SQLite database. + * + * @param array $assoc_args Associative arguments (unused for SQLite). + */ + protected function sqlite_drop( $assoc_args ) { + $db_path = $this->get_sqlite_db_path(); + + if ( ! file_exists( $db_path ) ) { + WP_CLI::error( 'Database does not exist.' ); + } + + if ( ! unlink( $db_path ) ) { + WP_CLI::error( "Could not delete database file: {$db_path}" ); + } + + WP_CLI::success( 'Database dropped.' ); + } + + /** + * Reset SQLite database. + * + * @param array $assoc_args Associative arguments (unused for SQLite). + */ + protected function sqlite_reset( $assoc_args ) { + $db_path = $this->get_sqlite_db_path(); + + // Delete if exists. + if ( file_exists( $db_path ) ) { + if ( ! unlink( $db_path ) ) { + WP_CLI::error( "Could not delete database file: {$db_path}" ); + } + } + + // Recreate. + $this->sqlite_create( $assoc_args ); + + WP_CLI::success( 'Database reset.' ); + } + + /** + * Execute a query against the SQLite database. + * + * @param string $query SQL query to execute. + * @param array $assoc_args Associative arguments. + */ + protected function sqlite_query( $query, $assoc_args ) { + $pdo = $this->get_sqlite_pdo(); + + if ( ! $pdo ) { + WP_CLI::error( 'Could not connect to SQLite database.' ); + } + + try { + $is_row_modifying_query = preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE)\b/i', $query ); + + if ( $is_row_modifying_query ) { + $stmt = $pdo->prepare( $query ); + $stmt->execute(); + $affected_rows = $stmt->rowCount(); + WP_CLI::success( "Query succeeded. Rows affected: {$affected_rows}" ); + } else { + $stmt = $pdo->query( $query ); + + // Fetch and display results. + $results = $stmt->fetchAll( PDO::FETCH_ASSOC ); + + if ( empty( $results ) ) { + // No results to display. + return; + } + + // Display as a table similar to MySQL output. + $headers = array_keys( $results[0] ); + $this->display_table( $headers, $results ); + } + } catch ( PDOException $e ) { + WP_CLI::error( 'Query failed: ' . $e->getMessage() ); + } + } + + /** + * Display results as a table similar to MySQL output. + * + * @param array $headers Column headers. + * @param array $rows Data rows. + */ + protected function display_table( $headers, $rows ) { + // Calculate column widths. + $widths = []; + foreach ( $headers as $header ) { + $widths[ $header ] = strlen( $header ); + } + + foreach ( $rows as $row ) { + foreach ( $row as $key => $value ) { + $widths[ $key ] = max( $widths[ $key ], strlen( (string) $value ) ); + } + } + + // Display header. + $separator = '+'; + $header_line = '|'; + foreach ( $headers as $header ) { + $separator .= str_repeat( '-', $widths[ $header ] + 2 ) . '+'; + $header_line .= ' ' . str_pad( $header, $widths[ $header ] ) . ' |'; + } + + WP_CLI::line( $separator ); + WP_CLI::line( $header_line ); + WP_CLI::line( $separator ); + + // Display rows. + foreach ( $rows as $row ) { + $row_line = '|'; + foreach ( $headers as $header ) { + $value = isset( $row[ $header ] ) ? $row[ $header ] : ''; + $row_line .= ' ' . str_pad( (string) $value, $widths[ $header ] ) . ' |'; + } + WP_CLI::line( $row_line ); + } + + WP_CLI::line( $separator ); + } + + /** + * Export SQLite database. + * + * @param string $file Output file path. + * @param array $assoc_args Associative arguments. + */ + protected function sqlite_export( $file, $assoc_args ) { + $db_path = $this->get_sqlite_db_path(); + + if ( ! file_exists( $db_path ) ) { + WP_CLI::error( 'Database does not exist.' ); + } + + $pdo = $this->get_sqlite_pdo(); + if ( ! $pdo ) { + WP_CLI::error( 'Could not connect to SQLite database.' ); + } + + $stdout = ( '-' === $file ); + + if ( $stdout ) { + $output = fopen( 'php://stdout', 'w' ); + } else { + $output = fopen( $file, 'w' ); + if ( ! $output ) { + WP_CLI::error( "Could not open file for writing: {$file}" ); + } + } + + try { + // Export schema and data as SQL. + fwrite( $output, "-- SQLite database dump\n" ); + fwrite( $output, "-- Database: " . basename( $db_path ) . "\n\n" ); + + // Get all tables. + $tables = $pdo->query( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name" )->fetchAll( PDO::FETCH_COLUMN ); + + foreach ( $tables as $table ) { + // Get CREATE TABLE statement. + $create_stmt = $pdo->query( "SELECT sql FROM sqlite_master WHERE type='table' AND name=" . $pdo->quote( $table ) )->fetchColumn(); + + if ( isset( $assoc_args['add-drop-table'] ) ) { + fwrite( $output, "DROP TABLE IF EXISTS {$table};\n" ); + } + + fwrite( $output, $create_stmt . ";\n\n" ); + + // Export data. + $rows = $pdo->query( "SELECT * FROM {$table}" )->fetchAll( PDO::FETCH_ASSOC ); + + foreach ( $rows as $row ) { + $columns = array_keys( $row ); + $values = array_map( [ $pdo, 'quote' ], array_values( $row ) ); + + fwrite( $output, "INSERT INTO {$table} (" . implode( ', ', $columns ) . ') VALUES (' . implode( ', ', $values ) . ");\n" ); + } + + fwrite( $output, "\n" ); + } + } catch ( PDOException $e ) { + fclose( $output ); + WP_CLI::error( 'Export failed: ' . $e->getMessage() ); + } + + fclose( $output ); + + if ( ! $stdout ) { + if ( isset( $assoc_args['porcelain'] ) ) { + WP_CLI::line( $file ); + } else { + WP_CLI::success( "Exported to '{$file}'." ); + } + } + } + + /** + * Import SQL into SQLite database. + * + * @param string $file Input file path. + * @param array $assoc_args Associative arguments. + */ + protected function sqlite_import( $file, $assoc_args ) { + $pdo = $this->get_sqlite_pdo(); + + if ( ! $pdo ) { + WP_CLI::error( 'Could not connect to SQLite database.' ); + } + + if ( '-' === $file ) { + $sql = stream_get_contents( STDIN ); + } else { + if ( ! is_readable( $file ) ) { + WP_CLI::error( sprintf( 'Import file missing or not readable: %s', $file ) ); + } + $sql = file_get_contents( $file ); + } + + if ( false === $sql ) { + WP_CLI::error( 'Could not read import file.' ); + } + + try { + // Split SQL into individual statements. + $statements = array_filter( + array_map( + 'trim', + preg_split( '/;[\r\n]+/', $sql ) + ) + ); + + $pdo->beginTransaction(); + + foreach ( $statements as $statement ) { + if ( empty( $statement ) || 0 === strpos( $statement, '--' ) ) { + continue; + } + + $pdo->exec( $statement ); + } + + $pdo->commit(); + + WP_CLI::success( sprintf( "Imported from '%s'.", $file ) ); + } catch ( PDOException $e ) { + $pdo->rollBack(); + WP_CLI::error( 'Import failed: ' . $e->getMessage() ); + } + } + + /** + * Get SQLite database size. + * + * @return int Database file size in bytes, or 0 if not found. + */ + protected function sqlite_size() { + $db_path = $this->get_sqlite_db_path(); + + if ( ! file_exists( $db_path ) ) { + return 0; + } + + return filesize( $db_path ); + } + + /** + * Load WordPress db.php drop-in if SQLite is detected. + * + * This should be called early in commands that run at after_wp_config_load. + */ + protected function maybe_load_sqlite_dropin() { + if ( ! $this->is_sqlite() ) { + return; + } + + // Check if already loaded. + if ( defined( 'SQLITE_DB_DROPIN_VERSION' ) ) { + return; + } + + $wp_content_dir = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : ABSPATH . 'wp-content'; + $db_dropin_path = $wp_content_dir . '/db.php'; + + if ( ! file_exists( $db_dropin_path ) ) { + return; + } + + // Load required WordPress files if not already loaded. + if ( ! function_exists( 'add_action' ) ) { + $wpinc = defined( 'WPINC' ) ? WPINC : 'wp-includes'; + + $required_files = [ + ABSPATH . $wpinc . '/compat.php', + ABSPATH . $wpinc . '/plugin.php', + ABSPATH . $wpinc . '/class-wpdb.php', + ]; + + foreach ( $required_files as $required_file ) { + if ( file_exists( $required_file ) ) { + require_once $required_file; + } + } + } + + // Load the db.php drop-in. + require_once $db_dropin_path; + } +} From 711e37ba9f3949e6bde0dcd057aacb034cd2030b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:33:07 +0000 Subject: [PATCH 03/20] Fix sqlite_reset to not double-call sqlite_create Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/DB_Command_SQLite.php | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 2f080d8a..e6f1d764 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -169,8 +169,24 @@ protected function sqlite_reset( $assoc_args ) { } } - // Recreate. - $this->sqlite_create( $assoc_args ); + // Create directory if needed. + $db_dir = dirname( $db_path ); + if ( ! is_dir( $db_dir ) ) { + if ( ! mkdir( $db_dir, 0755, true ) ) { + WP_CLI::error( "Could not create directory: {$db_dir}" ); + } + } + + // Recreate the SQLite database file. + try { + $pdo = new PDO( 'sqlite:' . $db_path ); + $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); + // Execute a simple query to initialize the database. + $pdo->exec( 'CREATE TABLE IF NOT EXISTS _wpcli_test (id INTEGER)' ); + $pdo->exec( 'DROP TABLE _wpcli_test' ); + } catch ( PDOException $e ) { + WP_CLI::error( 'Could not create SQLite database: ' . $e->getMessage() ); + } WP_CLI::success( 'Database reset.' ); } From f7c116520399b39b04399d013972d8929428bb26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:34:30 +0000 Subject: [PATCH 04/20] Add SQLite test scenarios and documentation Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- README.md | 38 ++++++++++++++ features/db-sqlite.feature | 100 +++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 features/db-sqlite.feature diff --git a/README.md b/README.md index 547fa52a..da6e8a91 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,44 @@ Performs basic database operations using credentials stored in wp-config.php. Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) | [Support](#support) +## SQLite Support + +This package now supports SQLite databases in addition to MySQL/MariaDB. When using the [SQLite Database Integration plugin](https://github.com/WordPress/sqlite-database-integration/), most `wp db` commands will automatically detect and work with SQLite databases. + +**Supported commands with SQLite:** +- `wp db create` - Creates the SQLite database file +- `wp db drop` - Deletes the SQLite database file +- `wp db reset` - Recreates the SQLite database file +- `wp db query` - Executes SQL queries via PDO +- `wp db export` - Exports the SQLite database to SQL +- `wp db import` - Imports SQL into the SQLite database +- `wp db tables` - Lists tables (via $wpdb) +- `wp db size` - Shows database file size +- `wp db prefix` - Shows table prefix (via $wpdb) +- `wp db columns` - Shows column information (via $wpdb) +- `wp db search` - Searches database content (via $wpdb) +- `wp db clean` - Removes tables with prefix (via $wpdb) + +**Commands not applicable to SQLite:** +- `wp db check` - Shows a warning (SQLite doesn't require manual checking) +- `wp db optimize` - Shows a warning (SQLite auto-optimizes with VACUUM) +- `wp db repair` - Shows a warning (SQLite doesn't require manual repair) +- `wp db cli` - Shows a warning (use `wp db query` instead) + +**SQLite Detection:** + +The command automatically detects SQLite databases when: +1. The `DB_ENGINE` constant is set to `'sqlite'` in wp-config.php +2. The `SQLITE_DB_DROPIN_VERSION` constant is defined (plugin loaded) +3. A `db.php` drop-in exists with SQLite integration + +**Database File Location:** + +By default, the SQLite database is expected at `wp-content/database/.ht.sqlite`. You can customize this location using: +- `FQDB` constant - Full path to the database file +- `FQDBDIR` constant - Directory containing the database +- `DB_FILE` constant - Database filename (used with FQDBDIR) + ## Using This package implements the following commands: diff --git a/features/db-sqlite.feature b/features/db-sqlite.feature new file mode 100644 index 00000000..459c4e82 --- /dev/null +++ b/features/db-sqlite.feature @@ -0,0 +1,100 @@ +Feature: Perform database operations with SQLite + + @require-sqlite + Scenario: SQLite DB CRUD operations + Given a WP install with SQLite + And a session_yes file: + """ + y + """ + + When I run `wp db create` + Then the return code should be 1 + And STDERR should contain: + """ + Database already exists + """ + + When I run `wp db size` + Then STDOUT should contain: + """ + .ht.sqlite + """ + + When I run `wp db tables` + Then STDOUT should contain: + """ + wp_posts + """ + + @require-sqlite + Scenario: SQLite DB query + Given a WP install with SQLite + + When I run `wp db query 'SELECT COUNT(*) as total FROM wp_posts'` + Then STDOUT should contain: + """ + total + """ + + @require-sqlite + Scenario: SQLite DB export/import + Given a WP install with SQLite + + When I run `wp post list --format=count` + Then STDOUT should contain: + """ + 1 + """ + + When I run `wp db export /tmp/wp-cli-sqlite-behat.sql` + Then STDOUT should contain: + """ + Success: Exported + """ + + When I run `wp db reset < session_yes` + Then STDOUT should contain: + """ + Success: Database reset + """ + + When I run `wp db import /tmp/wp-cli-sqlite-behat.sql` + Then STDOUT should contain: + """ + Success: Imported + """ + + When I run `wp post list --format=count` + Then STDOUT should contain: + """ + 1 + """ + + @require-sqlite + Scenario: SQLite commands that show warnings + Given a WP install with SQLite + + When I run `wp db check` + Then STDOUT should contain: + """ + Warning: Database check is not supported for SQLite databases + """ + + When I run `wp db optimize` + Then STDOUT should contain: + """ + Warning: Database optimization is not supported for SQLite databases + """ + + When I run `wp db repair` + Then STDOUT should contain: + """ + Warning: Database repair is not supported for SQLite databases + """ + + When I run `wp db cli` + Then STDOUT should contain: + """ + Warning: Interactive console (cli) is not supported for SQLite databases + """ From 0d1fb30d031149e15cd76af8f8024756565dbacc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:36:50 +0000 Subject: [PATCH 05/20] Improve identifier escaping in SQLite export Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/DB_Command_SQLite.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index e6f1d764..95211278 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -314,23 +314,26 @@ protected function sqlite_export( $file, $assoc_args ) { $tables = $pdo->query( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name" )->fetchAll( PDO::FETCH_COLUMN ); foreach ( $tables as $table ) { + // Escape table name for identifiers. + $escaped_table = '"' . str_replace( '"', '""', $table ) . '"'; + // Get CREATE TABLE statement. $create_stmt = $pdo->query( "SELECT sql FROM sqlite_master WHERE type='table' AND name=" . $pdo->quote( $table ) )->fetchColumn(); if ( isset( $assoc_args['add-drop-table'] ) ) { - fwrite( $output, "DROP TABLE IF EXISTS {$table};\n" ); + fwrite( $output, "DROP TABLE IF EXISTS {$escaped_table};\n" ); } fwrite( $output, $create_stmt . ";\n\n" ); // Export data. - $rows = $pdo->query( "SELECT * FROM {$table}" )->fetchAll( PDO::FETCH_ASSOC ); + $rows = $pdo->query( "SELECT * FROM {$escaped_table}" )->fetchAll( PDO::FETCH_ASSOC ); foreach ( $rows as $row ) { $columns = array_keys( $row ); $values = array_map( [ $pdo, 'quote' ], array_values( $row ) ); - fwrite( $output, "INSERT INTO {$table} (" . implode( ', ', $columns ) . ') VALUES (' . implode( ', ', $values ) . ");\n" ); + fwrite( $output, "INSERT INTO {$escaped_table} (" . implode( ', ', $columns ) . ') VALUES (' . implode( ', ', $values ) . ");\n" ); } fwrite( $output, "\n" ); From 925ed48e8f9650dce0f9ad63affb23a5b9209f99 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 23 Nov 2025 12:16:12 +0100 Subject: [PATCH 06/20] Undo readme change --- README.md | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/README.md b/README.md index da6e8a91..547fa52a 100644 --- a/README.md +++ b/README.md @@ -7,44 +7,6 @@ Performs basic database operations using credentials stored in wp-config.php. Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) | [Support](#support) -## SQLite Support - -This package now supports SQLite databases in addition to MySQL/MariaDB. When using the [SQLite Database Integration plugin](https://github.com/WordPress/sqlite-database-integration/), most `wp db` commands will automatically detect and work with SQLite databases. - -**Supported commands with SQLite:** -- `wp db create` - Creates the SQLite database file -- `wp db drop` - Deletes the SQLite database file -- `wp db reset` - Recreates the SQLite database file -- `wp db query` - Executes SQL queries via PDO -- `wp db export` - Exports the SQLite database to SQL -- `wp db import` - Imports SQL into the SQLite database -- `wp db tables` - Lists tables (via $wpdb) -- `wp db size` - Shows database file size -- `wp db prefix` - Shows table prefix (via $wpdb) -- `wp db columns` - Shows column information (via $wpdb) -- `wp db search` - Searches database content (via $wpdb) -- `wp db clean` - Removes tables with prefix (via $wpdb) - -**Commands not applicable to SQLite:** -- `wp db check` - Shows a warning (SQLite doesn't require manual checking) -- `wp db optimize` - Shows a warning (SQLite auto-optimizes with VACUUM) -- `wp db repair` - Shows a warning (SQLite doesn't require manual repair) -- `wp db cli` - Shows a warning (use `wp db query` instead) - -**SQLite Detection:** - -The command automatically detects SQLite databases when: -1. The `DB_ENGINE` constant is set to `'sqlite'` in wp-config.php -2. The `SQLITE_DB_DROPIN_VERSION` constant is defined (plugin loaded) -3. A `db.php` drop-in exists with SQLite integration - -**Database File Location:** - -By default, the SQLite database is expected at `wp-content/database/.ht.sqlite`. You can customize this location using: -- `FQDB` constant - Full path to the database file -- `FQDBDIR` constant - Directory containing the database -- `DB_FILE` constant - Database filename (used with FQDBDIR) - ## Using This package implements the following commands: From 8c4e96fee114430b12b2bf09cc2e2c18d0d340b5 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 23 Nov 2025 12:16:17 +0100 Subject: [PATCH 07/20] Fix formatting --- src/DB_Command.php | 2 +- src/DB_Command_SQLite.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 347e7057..c11b24b2 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1210,7 +1210,7 @@ public function size( $args, $assoc_args ) { DB_NAME ) ); - $db_name = DB_NAME; + $db_name = DB_NAME; } // Add the database size to the list. diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 95211278..d4cac3dc 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -208,7 +208,7 @@ protected function sqlite_query( $query, $assoc_args ) { $is_row_modifying_query = preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE)\b/i', $query ); if ( $is_row_modifying_query ) { - $stmt = $pdo->prepare( $query ); + $stmt = $pdo->prepare( $query ); $stmt->execute(); $affected_rows = $stmt->rowCount(); WP_CLI::success( "Query succeeded. Rows affected: {$affected_rows}" ); @@ -252,10 +252,10 @@ protected function display_table( $headers, $rows ) { } // Display header. - $separator = '+'; + $separator = '+'; $header_line = '|'; foreach ( $headers as $header ) { - $separator .= str_repeat( '-', $widths[ $header ] + 2 ) . '+'; + $separator .= str_repeat( '-', $widths[ $header ] + 2 ) . '+'; $header_line .= ' ' . str_pad( $header, $widths[ $header ] ) . ' |'; } @@ -308,7 +308,7 @@ protected function sqlite_export( $file, $assoc_args ) { try { // Export schema and data as SQL. fwrite( $output, "-- SQLite database dump\n" ); - fwrite( $output, "-- Database: " . basename( $db_path ) . "\n\n" ); + fwrite( $output, '-- Database: ' . basename( $db_path ) . "\n\n" ); // Get all tables. $tables = $pdo->query( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name" )->fetchAll( PDO::FETCH_COLUMN ); From 35a91baaabad4f080891ffee5301ba28af29ef88 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 23 Nov 2025 12:24:15 +0100 Subject: [PATCH 08/20] PHPCS fixes --- phpcs.xml.dist | 7 +++++++ src/DB_Command.php | 10 +++++----- src/DB_Command_SQLite.php | 22 +++++++--------------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index c39cec40..a398187a 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -55,6 +55,13 @@ */src/DB_Command\.php$ + + */src/DB_Command_SQLite\.php$ + + + + */src/DB_Command_SQLite\.php$ + /tests/phpstan/scan-files diff --git a/src/DB_Command.php b/src/DB_Command.php index c11b24b2..7cbb530c 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -89,7 +89,7 @@ public function create( $_, $assoc_args ) { $this->maybe_load_sqlite_dropin(); if ( $this->is_sqlite() ) { - $this->sqlite_create( $assoc_args ); + $this->sqlite_create(); return; } @@ -130,7 +130,7 @@ public function drop( $_, $assoc_args ) { if ( $this->is_sqlite() ) { $db_path = $this->get_sqlite_db_path(); WP_CLI::confirm( "Are you sure you want to drop the SQLite database at '{$db_path}'?", $assoc_args ); - $this->sqlite_drop( $assoc_args ); + $this->sqlite_drop(); return; } @@ -173,7 +173,7 @@ public function reset( $_, $assoc_args ) { if ( $this->is_sqlite() ) { $db_path = $this->get_sqlite_db_path(); WP_CLI::confirm( "Are you sure you want to reset the SQLite database at '{$db_path}'?", $assoc_args ); - $this->sqlite_reset( $assoc_args ); + $this->sqlite_reset(); return; } @@ -568,7 +568,7 @@ public function query( $args, $assoc_args ) { WP_CLI::error( 'No query specified.' ); } - $this->sqlite_query( $query, $assoc_args ); + $this->sqlite_query( $query ); return; } @@ -888,7 +888,7 @@ public function import( $args, $assoc_args ) { } if ( $this->is_sqlite() ) { - $this->sqlite_import( $result_file, $assoc_args ); + $this->sqlite_import( $result_file ); return; } diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index d4cac3dc..89379580 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -102,10 +102,8 @@ protected function get_sqlite_pdo() { /** * Create SQLite database. - * - * @param array $assoc_args Associative arguments (unused for SQLite). */ - protected function sqlite_create( $assoc_args ) { + protected function sqlite_create() { $db_path = $this->get_sqlite_db_path(); $db_dir = dirname( $db_path ); @@ -137,10 +135,8 @@ protected function sqlite_create( $assoc_args ) { /** * Drop SQLite database. - * - * @param array $assoc_args Associative arguments (unused for SQLite). */ - protected function sqlite_drop( $assoc_args ) { + protected function sqlite_drop() { $db_path = $this->get_sqlite_db_path(); if ( ! file_exists( $db_path ) ) { @@ -156,10 +152,8 @@ protected function sqlite_drop( $assoc_args ) { /** * Reset SQLite database. - * - * @param array $assoc_args Associative arguments (unused for SQLite). */ - protected function sqlite_reset( $assoc_args ) { + protected function sqlite_reset() { $db_path = $this->get_sqlite_db_path(); // Delete if exists. @@ -194,10 +188,9 @@ protected function sqlite_reset( $assoc_args ) { /** * Execute a query against the SQLite database. * - * @param string $query SQL query to execute. - * @param array $assoc_args Associative arguments. + * @param string $query SQL query to execute. */ - protected function sqlite_query( $query, $assoc_args ) { + protected function sqlite_query( $query ) { $pdo = $this->get_sqlite_pdo(); if ( ! $pdo ) { @@ -357,10 +350,9 @@ protected function sqlite_export( $file, $assoc_args ) { /** * Import SQL into SQLite database. * - * @param string $file Input file path. - * @param array $assoc_args Associative arguments. + * @param string $file Input file path. */ - protected function sqlite_import( $file, $assoc_args ) { + protected function sqlite_import( $file ) { $pdo = $this->get_sqlite_pdo(); if ( ! $pdo ) { From a89fe310a98b4a527d3dc3f9ced495eca486f5e0 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 23 Nov 2025 12:41:53 +0100 Subject: [PATCH 09/20] PHPStan fixes --- src/DB_Command.php | 3 +- src/DB_Command_SQLite.php | 69 +++++++++++++++++++++++++++++++++------ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 7cbb530c..b3ead9df 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1202,7 +1202,8 @@ public function size( $args, $assoc_args ) { // Get the database size. if ( $is_sqlite ) { $db_bytes = $this->sqlite_size(); - $db_name = basename( $this->get_sqlite_db_path() ); + $db_path = $this->get_sqlite_db_path(); + $db_name = $db_path ? basename( $db_path ) : ''; } else { $db_bytes = $wpdb->get_var( $wpdb->prepare( diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 89379580..861e3cb5 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -105,7 +105,10 @@ protected function get_sqlite_pdo() { */ protected function sqlite_create() { $db_path = $this->get_sqlite_db_path(); - $db_dir = dirname( $db_path ); + if ( ! $db_path ) { + WP_CLI::error( 'Could not determine the database path.' ); + } + $db_dir = dirname( $db_path ); // Create directory if it doesn't exist. if ( ! is_dir( $db_dir ) ) { @@ -139,6 +142,10 @@ protected function sqlite_create() { protected function sqlite_drop() { $db_path = $this->get_sqlite_db_path(); + if ( ! $db_path ) { + WP_CLI::error( 'Could not determine the database path.' ); + } + if ( ! file_exists( $db_path ) ) { WP_CLI::error( 'Database does not exist.' ); } @@ -156,6 +163,10 @@ protected function sqlite_drop() { protected function sqlite_reset() { $db_path = $this->get_sqlite_db_path(); + if ( ! $db_path ) { + WP_CLI::error( 'Could not determine the database path.' ); + } + // Delete if exists. if ( file_exists( $db_path ) ) { if ( ! unlink( $db_path ) ) { @@ -208,6 +219,12 @@ protected function sqlite_query( $query ) { } else { $stmt = $pdo->query( $query ); + if ( ! $stmt ) { + // There was an error. + $error_info = $pdo->errorInfo(); + WP_CLI::error( 'Query failed: ' . $error_info[2] ); + } + // Fetch and display results. $results = $stmt->fetchAll( PDO::FETCH_ASSOC ); @@ -278,6 +295,10 @@ protected function display_table( $headers, $rows ) { protected function sqlite_export( $file, $assoc_args ) { $db_path = $this->get_sqlite_db_path(); + if ( ! $db_path ) { + WP_CLI::error( 'Could not determine the database path.' ); + } + if ( ! file_exists( $db_path ) ) { WP_CLI::error( 'Database does not exist.' ); } @@ -293,9 +314,10 @@ protected function sqlite_export( $file, $assoc_args ) { $output = fopen( 'php://stdout', 'w' ); } else { $output = fopen( $file, 'w' ); - if ( ! $output ) { - WP_CLI::error( "Could not open file for writing: {$file}" ); - } + } + + if ( ! $output ) { + WP_CLI::error( "Could not open file for writing: {$file}" ); } try { @@ -304,14 +326,26 @@ protected function sqlite_export( $file, $assoc_args ) { fwrite( $output, '-- Database: ' . basename( $db_path ) . "\n\n" ); // Get all tables. - $tables = $pdo->query( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name" )->fetchAll( PDO::FETCH_COLUMN ); + $stmt = $pdo->query( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name" ); + if ( ! $stmt ) { + // There was an error. + $error_info = $pdo->errorInfo(); + WP_CLI::error( 'Could not retrieve table list: ' . $error_info[2] ); + } + $tables = $stmt->fetchAll( PDO::FETCH_COLUMN ); foreach ( $tables as $table ) { // Escape table name for identifiers. $escaped_table = '"' . str_replace( '"', '""', $table ) . '"'; // Get CREATE TABLE statement. - $create_stmt = $pdo->query( "SELECT sql FROM sqlite_master WHERE type='table' AND name=" . $pdo->quote( $table ) )->fetchColumn(); + $stmt = $pdo->query( "SELECT sql FROM sqlite_master WHERE type='table' AND name=" . $pdo->quote( $table ) ); + if ( ! $stmt ) { + // There was an error. + $error_info = $pdo->errorInfo(); + WP_CLI::error( "Could not retrieve CREATE TABLE statement for table {$escaped_table}: " . $error_info[2] ); + } + $create_stmt = $stmt->fetchColumn(); if ( isset( $assoc_args['add-drop-table'] ) ) { fwrite( $output, "DROP TABLE IF EXISTS {$escaped_table};\n" ); @@ -320,7 +354,13 @@ protected function sqlite_export( $file, $assoc_args ) { fwrite( $output, $create_stmt . ";\n\n" ); // Export data. - $rows = $pdo->query( "SELECT * FROM {$escaped_table}" )->fetchAll( PDO::FETCH_ASSOC ); + $stmt = $pdo->query( "SELECT * FROM {$escaped_table}" ); + if ( ! $stmt ) { + // There was an error. + $error_info = $pdo->errorInfo(); + WP_CLI::error( "Could not retrieve data for table {$escaped_table}: " . $error_info[2] ); + } + $rows = $stmt->fetchAll( PDO::FETCH_ASSOC ); foreach ( $rows as $row ) { $columns = array_keys( $row ); @@ -374,10 +414,14 @@ protected function sqlite_import( $file ) { try { // Split SQL into individual statements. + $lines = preg_split( '/;[\r\n]+/', $sql ); + if ( ! is_array( $lines ) ) { + $lines = []; + } $statements = array_filter( array_map( 'trim', - preg_split( '/;[\r\n]+/', $sql ) + $lines ) ); @@ -408,11 +452,16 @@ protected function sqlite_import( $file ) { protected function sqlite_size() { $db_path = $this->get_sqlite_db_path(); - if ( ! file_exists( $db_path ) ) { + if ( ! $db_path || ! file_exists( $db_path ) ) { + return 0; + } + + $size = filesize( $db_path ); + if ( false === $size ) { return 0; } - return filesize( $db_path ); + return $size; } /** From 19f681381a7780171eae4a6a3e064cac2c603d9c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 23 Nov 2025 12:46:04 +0100 Subject: [PATCH 10/20] Some test improvements --- features/db-check.feature | 14 +++++ features/db-cli.feature | 21 ++++++++ features/db-create.feature | 24 +++++++++ features/db-optimize.feature | 21 ++++++++ features/db-repair.feature | 21 ++++++++ features/db-sqlite.feature | 100 ----------------------------------- features/db.feature | 82 +++++++++++++++++++++++++++- 7 files changed, 182 insertions(+), 101 deletions(-) create mode 100644 features/db-cli.feature create mode 100644 features/db-create.feature create mode 100644 features/db-optimize.feature create mode 100644 features/db-repair.feature delete mode 100644 features/db-sqlite.feature diff --git a/features/db-check.feature b/features/db-check.feature index aa5ce47b..51b7a17f 100644 --- a/features/db-check.feature +++ b/features/db-check.feature @@ -1,5 +1,6 @@ Feature: Check the database + @require-mysql-or-mariadb Scenario: Run db check to check the database Given a WP install @@ -13,6 +14,7 @@ Feature: Check the database Success: Database checked. """ + @require-mysql-or-mariadb Scenario: Run db check with MySQL defaults to check the database Given a WP install @@ -26,6 +28,7 @@ Feature: Check the database Success: Database checked. """ + @require-mysql-or-mariadb Scenario: Run db check with --no-defaults to check the database Given a WP install @@ -39,6 +42,7 @@ Feature: Check the database Success: Database checked. """ + @require-mysql-or-mariadb Scenario: Run db check with passed-in options Given a WP install @@ -124,6 +128,7 @@ Feature: Check the database """ And STDOUT should be empty + @require-mysql-or-mariadb Scenario: MySQL defaults are available as appropriate with --defaults flag Given a WP install @@ -136,3 +141,12 @@ Feature: Check the database When I try `wp db check --no-defaults --debug` Then STDERR should match #Debug \(db\): Running shell command: /usr/bin/env (mysqlcheck|mariadb-check) --no-defaults %s# + @require-sqlite + Scenario: SQLite commands that show warnings + Given a WP install + + When I run `wp db check` + Then STDOUT should contain: + """ + Warning: Database check is not supported for SQLite databases + """ diff --git a/features/db-cli.feature b/features/db-cli.feature new file mode 100644 index 00000000..244a9078 --- /dev/null +++ b/features/db-cli.feature @@ -0,0 +1,21 @@ +Feature: Open a MySQL console + + @require-mysql-or-mariadb + Scenario: Run db cli to open a MySQL console + Given a WP install + + When I run `wp db cli` + Then STDOUT should contain: + """ + mysql> + """ + + @require-sqlite + Scenario: SQLite commands that show warnings for cli + Given a WP install + + When I run `wp db cli` + Then STDOUT should contain: + """ + Warning: Interactive console (cli) is not supported for SQLite databases + """ diff --git a/features/db-create.feature b/features/db-create.feature new file mode 100644 index 00000000..8b90873a --- /dev/null +++ b/features/db-create.feature @@ -0,0 +1,24 @@ +Feature: Create a new database + + @require-mysql-or-mariadb + Scenario: Create a new database + Given an empty directory + And WP files + And wp-config.php + + When I run `wp db create` + Then STDOUT should contain: + """ + Success: Database created. + """ + + @require-sqlite + Scenario: SQLite DB create operation should fail if already existing + Given a WP install + + When I try `wp db create` + Then the return code should be 1 + And STDERR should contain: + """ + Database already exists + """ diff --git a/features/db-optimize.feature b/features/db-optimize.feature new file mode 100644 index 00000000..1217d494 --- /dev/null +++ b/features/db-optimize.feature @@ -0,0 +1,21 @@ +Feature: Optimize the database + + @require-mysql-or-mariadb + Scenario: Run db optimize to optimize the database + Given a WP install + + When I run `wp db optimize` + Then STDOUT should contain: + """ + Success: Database optimized. + """ + + @require-sqlite + Scenario: SQLite commands that show warnings for optimize + Given a WP install + + When I run `wp db optimize` + Then STDOUT should contain: + """ + Warning: Database optimization is not supported for SQLite databases + """ diff --git a/features/db-repair.feature b/features/db-repair.feature new file mode 100644 index 00000000..c0f92e47 --- /dev/null +++ b/features/db-repair.feature @@ -0,0 +1,21 @@ +Feature: Repair the database + + @require-mysql-or-mariadb + Scenario: Run db repair to repair the database + Given a WP install + + When I run `wp db repair` + Then STDOUT should contain: + """ + Success: Database repaired. + """ + + @require-sqlite + Scenario: SQLite commands that show warnings for repair + Given a WP install + + When I run `wp db repair` + Then STDOUT should contain: + """ + Warning: Database repair is not supported for SQLite databases + """ diff --git a/features/db-sqlite.feature b/features/db-sqlite.feature deleted file mode 100644 index 459c4e82..00000000 --- a/features/db-sqlite.feature +++ /dev/null @@ -1,100 +0,0 @@ -Feature: Perform database operations with SQLite - - @require-sqlite - Scenario: SQLite DB CRUD operations - Given a WP install with SQLite - And a session_yes file: - """ - y - """ - - When I run `wp db create` - Then the return code should be 1 - And STDERR should contain: - """ - Database already exists - """ - - When I run `wp db size` - Then STDOUT should contain: - """ - .ht.sqlite - """ - - When I run `wp db tables` - Then STDOUT should contain: - """ - wp_posts - """ - - @require-sqlite - Scenario: SQLite DB query - Given a WP install with SQLite - - When I run `wp db query 'SELECT COUNT(*) as total FROM wp_posts'` - Then STDOUT should contain: - """ - total - """ - - @require-sqlite - Scenario: SQLite DB export/import - Given a WP install with SQLite - - When I run `wp post list --format=count` - Then STDOUT should contain: - """ - 1 - """ - - When I run `wp db export /tmp/wp-cli-sqlite-behat.sql` - Then STDOUT should contain: - """ - Success: Exported - """ - - When I run `wp db reset < session_yes` - Then STDOUT should contain: - """ - Success: Database reset - """ - - When I run `wp db import /tmp/wp-cli-sqlite-behat.sql` - Then STDOUT should contain: - """ - Success: Imported - """ - - When I run `wp post list --format=count` - Then STDOUT should contain: - """ - 1 - """ - - @require-sqlite - Scenario: SQLite commands that show warnings - Given a WP install with SQLite - - When I run `wp db check` - Then STDOUT should contain: - """ - Warning: Database check is not supported for SQLite databases - """ - - When I run `wp db optimize` - Then STDOUT should contain: - """ - Warning: Database optimization is not supported for SQLite databases - """ - - When I run `wp db repair` - Then STDOUT should contain: - """ - Warning: Database repair is not supported for SQLite databases - """ - - When I run `wp db cli` - Then STDOUT should contain: - """ - Warning: Interactive console (cli) is not supported for SQLite databases - """ diff --git a/features/db.feature b/features/db.feature index dbfdb4b0..2d47ba1e 100644 --- a/features/db.feature +++ b/features/db.feature @@ -1,5 +1,6 @@ Feature: Perform database operations + @require-mysql-or-mariadb Scenario: DB CRUD Given an empty directory And WP files @@ -52,6 +53,7 @@ Feature: Perform database operations Are you sure you want to reset the 'wp_cli_test' database? [y/n] Success: Database reset. """ + @require-mysql-or-mariadb Scenario: DB CRUD with passed-in dbuser/dbpass Given an empty directory And WP files @@ -107,6 +109,7 @@ Feature: Perform database operations """ And STDOUT should be empty + @require-mysql-or-mariadb Scenario: Clean up a WordPress install without dropping its database entirely but tables with prefix. Given a WP install @@ -138,6 +141,7 @@ Feature: Perform database operations """ And the return code should be 0 + @require-mysql-or-mariadb Scenario: DB Operations Given a WP install @@ -147,6 +151,7 @@ Feature: Perform database operations When I run `wp db repair` Then STDOUT should not be empty + @require-mysql-or-mariadb Scenario: DB Operations with passed-in options Given a WP install @@ -185,6 +190,7 @@ Feature: Perform database operations """ And STDOUT should not be empty + @require-mysql-or-mariadb Scenario: DB Query Given a WP install @@ -214,6 +220,7 @@ Feature: Perform database operations home """ + @require-mysql-or-mariadb Scenario: DB export/import Given a WP install @@ -262,6 +269,7 @@ Feature: Perform database operations 1 """ + @require-mysql-or-mariadb Scenario: DB export no charset Given an empty directory And WP files @@ -327,6 +335,7 @@ Feature: Perform database operations latin1_spanish_ci """ + @require-mysql-or-mariadb Scenario: Row modifying queries should return the number of affected rows Given a WP install When I run `wp db query "UPDATE wp_users SET user_status = 1 WHERE ID = 1"` @@ -334,7 +343,7 @@ Feature: Perform database operations """ Query succeeded. Rows affected: 1 """ - + When I run `wp db query "SELECT * FROM wp_users WHERE ID = 1"` Then STDOUT should not contain: """ @@ -346,3 +355,74 @@ Feature: Perform database operations """ Query succeeded. Rows affected: 1 """ + + @require-sqlite + Scenario: SQLite DB CRUD operations + Given a WP install + And a session_yes file: + """ + y + """ + + When I try `wp db create` + Then the return code should be 1 + And STDERR should contain: + """ + Database already exists + """ + + When I run `wp db drop < session_yes` + Then STDOUT should contain: + """ + Success: Database dropped. + """ + + When I run `wp db reset < session_yes` + Then STDOUT should contain: + """ + Success: Database reset + """ + + @require-sqlite + Scenario: SQLite DB query + Given a WP install + + When I run `wp db query 'SELECT COUNT(*) as total FROM wp_posts'` + Then STDOUT should contain: + """ + total + """ + + @require-sqlite + Scenario: SQLite DB export/import + Given a WP install + + When I run `wp post list --format=count` + Then STDOUT should contain: + """ + 1 + """ + + When I run `wp db export /tmp/wp-cli-sqlite-behat.sql` + Then STDOUT should contain: + """ + Success: Exported + """ + + When I run `wp db reset < session_yes` + Then STDOUT should contain: + """ + Success: Database reset + """ + + When I run `wp db import /tmp/wp-cli-sqlite-behat.sql` + Then STDOUT should contain: + """ + Success: Imported + """ + + When I run `wp post list --format=count` + Then STDOUT should contain: + """ + 1 + """ From 63f78651bd49494af80a03dcb5f7a65d92d31ed9 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 23 Nov 2025 16:43:41 +0100 Subject: [PATCH 11/20] Remove test --- features/db-cli.feature | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/features/db-cli.feature b/features/db-cli.feature index 244a9078..8998c1e0 100644 --- a/features/db-cli.feature +++ b/features/db-cli.feature @@ -1,15 +1,5 @@ Feature: Open a MySQL console - @require-mysql-or-mariadb - Scenario: Run db cli to open a MySQL console - Given a WP install - - When I run `wp db cli` - Then STDOUT should contain: - """ - mysql> - """ - @require-sqlite Scenario: SQLite commands that show warnings for cli Given a WP install From 1105c5e151ad027db889324453f483da386c80e3 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 23 Nov 2025 22:17:07 +0100 Subject: [PATCH 12/20] Improve some tests --- features/db-check.feature | 4 ++-- features/db-cli.feature | 4 ++-- features/db-export.feature | 12 ++++++++++-- features/db-optimize.feature | 4 ++-- features/db-repair.feature | 4 ++-- features/db-size.feature | 32 ++++++++++++++++++++++++++++++++ src/DB_Command.php | 9 ++++----- src/DB_Command_SQLite.php | 2 ++ 8 files changed, 56 insertions(+), 15 deletions(-) diff --git a/features/db-check.feature b/features/db-check.feature index 51b7a17f..d1f7ba80 100644 --- a/features/db-check.feature +++ b/features/db-check.feature @@ -145,8 +145,8 @@ Feature: Check the database Scenario: SQLite commands that show warnings Given a WP install - When I run `wp db check` - Then STDOUT should contain: + When I try `wp db check` + Then STDERR should contain: """ Warning: Database check is not supported for SQLite databases """ diff --git a/features/db-cli.feature b/features/db-cli.feature index 8998c1e0..4ef8b885 100644 --- a/features/db-cli.feature +++ b/features/db-cli.feature @@ -4,8 +4,8 @@ Feature: Open a MySQL console Scenario: SQLite commands that show warnings for cli Given a WP install - When I run `wp db cli` - Then STDOUT should contain: + When I try `wp db cli` + Then STDERR should contain: """ Warning: Interactive console (cli) is not supported for SQLite databases """ diff --git a/features/db-export.feature b/features/db-export.feature index 370ae1df..edbe86f1 100644 --- a/features/db-export.feature +++ b/features/db-export.feature @@ -27,11 +27,19 @@ Feature: Export a WordPress database Then the wp_cli_test.sql file should exist And the wp_cli_test.sql file should not contain: """ - wp_users + CREATE TABLE wp_users """ And the wp_cli_test.sql file should contain: """ - wp_options + CREATE TABLE wp_options + """ + And the wp_cli_test.sql file should not contain: + """ + CREATE TABLE "wp_users" + """ + And the wp_cli_test.sql file should contain: + """ + CREATE TABLE "wp_options" """ Scenario: Export database to STDOUT diff --git a/features/db-optimize.feature b/features/db-optimize.feature index 1217d494..4fec46d3 100644 --- a/features/db-optimize.feature +++ b/features/db-optimize.feature @@ -14,8 +14,8 @@ Feature: Optimize the database Scenario: SQLite commands that show warnings for optimize Given a WP install - When I run `wp db optimize` - Then STDOUT should contain: + When I try `wp db optimize` + Then STDERR should contain: """ Warning: Database optimization is not supported for SQLite databases """ diff --git a/features/db-repair.feature b/features/db-repair.feature index c0f92e47..46104b21 100644 --- a/features/db-repair.feature +++ b/features/db-repair.feature @@ -14,8 +14,8 @@ Feature: Repair the database Scenario: SQLite commands that show warnings for repair Given a WP install - When I run `wp db repair` - Then STDOUT should contain: + When I try `wp db repair` + Then STDERR should contain: """ Warning: Database repair is not supported for SQLite databases """ diff --git a/features/db-size.feature b/features/db-size.feature index 0958bbf1..151d0e5b 100644 --- a/features/db-size.feature +++ b/features/db-size.feature @@ -2,6 +2,7 @@ Feature: Display database size + @require-mysql-or-mariadb Scenario: Display only database size for a WordPress install Given a WP install @@ -16,6 +17,21 @@ Feature: Display database size B """ + @require-sqlite + Scenario: Display only database size for a WordPress install + Given a WP install + + When I run `wp db size` + Then STDOUT should contain: + """ + .ht.sqlite + """ + + And STDOUT should contain: + """ + B + """ + Scenario: Display only table sizes for a WordPress install Given a WP install @@ -27,6 +43,7 @@ Feature: Display database size wp_cli_test """ + @require-mysql-or-mariadb Scenario: Display only database size in a human readable format for a WordPress install Given a WP install @@ -49,6 +66,21 @@ Feature: Display database size """ And STDOUT should be empty + @require-sqlite + Scenario: Display only database size in a human readable format for a WordPress install + Given a WP install + + When I run `wp db size --human-readable` + Then STDOUT should contain: + """ + .ht.sqlite + """ + + And STDOUT should contain: + """ + KB + """ + Scenario: Display only table sizes in a human readable format for a WordPress install Given a WP install diff --git a/src/DB_Command.php b/src/DB_Command.php index b3ead9df..a26eec55 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -708,8 +708,7 @@ public function export( $args, $assoc_args ) { } else { // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- WordPress is not loaded. $hash = substr( md5( (string) mt_rand() ), 0, 7 ); - $db_name = $this->is_sqlite() ? 'sqlite-db' : DB_NAME; - $result_file = sprintf( '%s-%s-%s.sql', $db_name, date( 'Y-m-d' ), $hash ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + $result_file = sprintf( '%s-%s-%s.sql', DB_NAME, date( 'Y-m-d' ), $hash ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } @@ -884,7 +883,7 @@ public function import( $args, $assoc_args ) { if ( ! empty( $args[0] ) ) { $result_file = $args[0]; } else { - $result_file = $this->is_sqlite() ? 'sqlite-db.sql' : sprintf( '%s.sql', DB_NAME ); + $result_file = sprintf( '%s.sql', DB_NAME ); } if ( $this->is_sqlite() ) { @@ -1614,8 +1613,8 @@ public function search( $args, $assoc_args ) { if ( ! $text_columns ) { if ( $stats ) { $skipped[] = $table; - // Don't bother warning for term relationships (which is just 3 int columns). - } elseif ( ! preg_match( '/_term_relationships$/', $table ) ) { + // Don't bother warning for term relationships (which is just 3 int columns) or SQLite. + } elseif ( ! preg_match( '/_term_relationships$/', $table ) && ! $this->is_sqlite() ) { WP_CLI::warning( $primary_keys ? "No text columns for table '$table' - skipped." : "No primary key or text columns for table '$table' - skipped." ); } continue; diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 861e3cb5..50f78cf9 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -371,6 +371,8 @@ protected function sqlite_export( $file, $assoc_args ) { fwrite( $output, "\n" ); } + + fwrite( $output, '-- Dump completed on ' . date( 'Y-m-d H:i:s' ) . "\n\n" ); } catch ( PDOException $e ) { fclose( $output ); WP_CLI::error( 'Export failed: ' . $e->getMessage() ); From b3bb6811f3c37389fb5b4439568e0622f2cbca23 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 23 Nov 2025 22:25:55 +0100 Subject: [PATCH 13/20] Use `gmdate` --- src/DB_Command_SQLite.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 50f78cf9..0d7df9a1 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -372,7 +372,7 @@ protected function sqlite_export( $file, $assoc_args ) { fwrite( $output, "\n" ); } - fwrite( $output, '-- Dump completed on ' . date( 'Y-m-d H:i:s' ) . "\n\n" ); + fwrite( $output, '-- Dump completed on ' . gmdate( 'Y-m-d H:i:s' ) . "\n\n" ); } catch ( PDOException $e ) { fclose( $output ); WP_CLI::error( 'Export failed: ' . $e->getMessage() ); From 75f52d280e53c6befb25cc95f6600e09fce971a0 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 23 Nov 2025 22:37:23 +0100 Subject: [PATCH 14/20] More test fixes --- features/db-export.feature | 21 ++++----------------- features/db-import.feature | 2 ++ features/db-query.feature | 1 + src/DB_Command_SQLite.php | 10 ++++++++++ 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/features/db-export.feature b/features/db-export.feature index edbe86f1..1f5cce45 100644 --- a/features/db-export.feature +++ b/features/db-export.feature @@ -23,24 +23,10 @@ Feature: Export a WordPress database Scenario: Exclude tables when exporting the database Given a WP install - When I run `wp db export wp_cli_test.sql --exclude_tables=wp_users --porcelain` + When I try `wp db export wp_cli_test.sql --exclude_tables=wp_users --porcelain` Then the wp_cli_test.sql file should exist - And the wp_cli_test.sql file should not contain: - """ - CREATE TABLE wp_users - """ - And the wp_cli_test.sql file should contain: - """ - CREATE TABLE wp_options - """ - And the wp_cli_test.sql file should not contain: - """ - CREATE TABLE "wp_users" - """ - And the wp_cli_test.sql file should contain: - """ - CREATE TABLE "wp_options" - """ + And the contents of the wp_cli_test.sql file should not match /CREATE TABLE "?wp_users"?/ + And the contents of the wp_cli_test.sql file should match /CREATE TABLE "?wp_options"?/ Scenario: Export database to STDOUT Given a WP install @@ -86,6 +72,7 @@ Feature: Export a WordPress database """ And STDOUT should be empty + @require-mysql-or-mariadb Scenario: MySQL defaults are available as appropriate with --defaults flag Given a WP install diff --git a/features/db-import.feature b/features/db-import.feature index 5761fb24..81dca007 100644 --- a/features/db-import.feature +++ b/features/db-import.feature @@ -127,6 +127,8 @@ Feature: Import a WordPress database """ wp db import """ + + @require-mysql-or-mariadb Scenario: MySQL defaults are available as appropriate with --defaults flag Given a WP install diff --git a/features/db-query.feature b/features/db-query.feature index 5831a536..2f518995 100644 --- a/features/db-query.feature +++ b/features/db-query.feature @@ -72,6 +72,7 @@ Feature: Query the database with WordPress' MySQL config """ And STDOUT should be empty + @require-mysql-or-mariadb Scenario: MySQL defaults are available as appropriate with --defaults flag Given a WP install diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 0d7df9a1..e6244a67 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -1,5 +1,7 @@ fetchAll( PDO::FETCH_COLUMN ); foreach ( $tables as $table ) { + if ( in_array( $table, $exclude_tables, true ) ) { + continue; + } + // Escape table name for identifiers. $escaped_table = '"' . str_replace( '"', '""', $table ) . '"'; From 8eeea431cfcfc3272f9e260950dab437f6c14c89 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 23 Nov 2025 22:46:39 +0100 Subject: [PATCH 15/20] Fix regex --- features/db-export.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/db-export.feature b/features/db-export.feature index 1f5cce45..9469196a 100644 --- a/features/db-export.feature +++ b/features/db-export.feature @@ -25,8 +25,8 @@ Feature: Export a WordPress database When I try `wp db export wp_cli_test.sql --exclude_tables=wp_users --porcelain` Then the wp_cli_test.sql file should exist - And the contents of the wp_cli_test.sql file should not match /CREATE TABLE "?wp_users"?/ - And the contents of the wp_cli_test.sql file should match /CREATE TABLE "?wp_options"?/ + And the contents of the wp_cli_test.sql file should not match /CREATE TABLE ["`]?wp_users["`]?/ + And the contents of the wp_cli_test.sql file should match /CREATE TABLE ["`]?wp_options["`]?/ Scenario: Export database to STDOUT Given a WP install From 0e1637ef6c6e616fc22e5489069303a5f009e42b Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 23 Nov 2025 22:52:12 +0100 Subject: [PATCH 16/20] More fixes --- features/db-import.feature | 31 ++++++++++++++++++++++++++++++- features/db-size.feature | 22 ++++++++++++++++++++++ src/DB_Command.php | 4 ++++ src/DB_Command_SQLite.php | 2 +- 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/features/db-import.feature b/features/db-import.feature index 81dca007..75ce74f3 100644 --- a/features/db-import.feature +++ b/features/db-import.feature @@ -144,7 +144,7 @@ Feature: Import a WordPress database When I try `wp db import --no-defaults --debug` Then STDERR should match #Debug \(db\): Running shell command: /usr/bin/env (mysql|mariadb) --no-defaults --no-auto-rehash# - @require-wp-4.2 + @require-wp-4.2 @require-mysql-or-mariadb Scenario: Import db that has emoji in post Given a WP install @@ -180,3 +180,32 @@ Feature: Import a WordPress database """ 🍣 """ + + @require-wp-4.2 @require-sqlite + Scenario: Import db that has emoji in post + Given a WP install + + When I run `wp post create --post_title="🍣"` + And I run `wp post list` + Then the return code should be 0 + And STDOUT should contain: + """ + 🍣 + """ + + When I try `wp db export wp_cli_test.sql --debug` + Then the return code should be 0 + And the wp_cli_test.sql file should exist + + When I run `wp db import --dbuser=wp_cli_test --dbpass=password1` + Then STDOUT should be: + """ + Success: Imported from 'wp_cli_test.sql'. + """ + + When I run `wp post list` + Then the return code should be 0 + And STDOUT should contain: + """ + 🍣 + """ diff --git a/features/db-size.feature b/features/db-size.feature index 151d0e5b..2728fc17 100644 --- a/features/db-size.feature +++ b/features/db-size.feature @@ -187,6 +187,7 @@ Feature: Display database size MB """ + @require-mysql-or-mariadb Scenario: Display database size in bytes with specific format for a WordPress install Given a WP install @@ -207,6 +208,27 @@ Feature: Display database size But STDOUT should not be a number + @require-sqlite + Scenario: Display database size in bytes with specific format for a WordPress install + Given a WP install + + When I run `wp db size --size_format=b --format=csv` + Then STDOUT should contain: + """ + Name,Size + .ht.sqlite," + """ + + But STDOUT should not be a number + + When I run `wp db size --size_format=b --format=json` + Then STDOUT should contain: + """ + [{"Name":".ht.sqlite","Size":" + """ + + But STDOUT should not be a number + Scenario: Display all table sizes for a WordPress install Given a WP install diff --git a/src/DB_Command.php b/src/DB_Command.php index a26eec55..6f24b708 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1244,6 +1244,10 @@ public function size( $args, $assoc_args ) { $size_key = floor( log( (float) $row['Size'] ) / log( 1000 ) ); $sizes = [ 'B', 'KB', 'MB', 'GB', 'TB' ]; + if ( is_infinite( $size_key ) ) { + $size_key = 0; + } + $size_format = isset( $sizes[ $size_key ] ) ? $sizes[ $size_key ] : $sizes[0]; } diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index e6244a67..d9d8b7dc 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -323,7 +323,7 @@ protected function sqlite_export( $file, $assoc_args ) { } $exclude_tables = Utils\get_flag_value( $assoc_args, 'exclude_tables', '' ); - $exclude_tables = explode( ',', trim( $assoc_args['exclude_tables'], ',' ) ); + $exclude_tables = explode( ',', trim( $exclude_tables, ',' ) ); $exclude_tables = array_map( 'strtolower', $exclude_tables ); try { From 1ab8410d1319196fc7b2e784d340992e42e3e9d1 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 24 Nov 2025 09:35:41 +0100 Subject: [PATCH 17/20] More fixes --- features/db-columns.feature | 12 ++++ features/db-export.feature | 19 ++++++ features/db-tables.feature | 118 +++++++++++++++++++++++++++++++++++- src/DB_Command.php | 16 +++-- src/DB_Command_SQLite.php | 3 +- 5 files changed, 162 insertions(+), 6 deletions(-) diff --git a/features/db-columns.feature b/features/db-columns.feature index b2e1ee76..40f541e2 100644 --- a/features/db-columns.feature +++ b/features/db-columns.feature @@ -41,6 +41,7 @@ Feature: Display information about a given table. Couldn't find any tables matching: wp_foobar """ + @require-mysql-or-mariadb Scenario: Display information about a non default WordPress table Given a WP install And I run `wp db query "CREATE TABLE not_wp ( date DATE NOT NULL, awesome_stuff TEXT, PRIMARY KEY (date) );;"` @@ -50,3 +51,14 @@ Feature: Display information about a given table. | Field | Type | Null | Key | Default | Extra | | date | date | NO | PRI | | | | awesome_stuff | text | YES | | | | + + @require-sqlite + Scenario: Display information about a non default WordPress table + Given a WP install + And I run `wp db query "CREATE TABLE not_wp ( date DATE NOT NULL, awesome_stuff TEXT, PRIMARY KEY (date) );;"` + + When I try `wp db columns not_wp` + Then STDOUT should be a table containing rows: + | Field | Type | Null | Key | Default | + | date | date | NO | PRI | | + | awesome_stuff | text | YES | | | diff --git a/features/db-export.feature b/features/db-export.feature index 9469196a..45dfd9ad 100644 --- a/features/db-export.feature +++ b/features/db-export.feature @@ -55,6 +55,7 @@ Feature: Export a WordPress database -- Dump completed on """ + @require-mysql-or-mariadb Scenario: Export database with passed-in options Given a WP install @@ -72,6 +73,24 @@ Feature: Export a WordPress database """ And STDOUT should be empty + @require-sqlite + Scenario: Export database with passed-in options + Given a WP install + + When I run `wp db export - --skip-comments` + Then STDOUT should not contain: + """ + -- Table structure + """ + + # dbpass has no effect on SQLite + When I try `wp db export - --dbpass=no_such_pass` + Then the return code should be 0 + And STDERR should not contain: + """ + Access denied + """ + @require-mysql-or-mariadb Scenario: MySQL defaults are available as appropriate with --defaults flag Given a WP install diff --git a/features/db-tables.feature b/features/db-tables.feature index af8cc6ac..8c93bc0d 100644 --- a/features/db-tables.feature +++ b/features/db-tables.feature @@ -1,5 +1,6 @@ Feature: List database tables + @require-mysql-or-mariadb Scenario: List database tables on a single WordPress install Given a WP install @@ -35,7 +36,42 @@ Feature: List database tables wp_postmeta,wp_posts """ - @require-wp-3.9 + @require-sqlite + Scenario: List database tables on a single WordPress install + Given a WP install + + When I run `wp db tables` + Then STDOUT should contain: + """ + _mysql_data_types_cache + wp_users + sqlite_sequence + wp_usermeta + wp_termmeta + wp_terms + wp_term_taxonomy + wp_term_relationships + wp_commentmeta + wp_comments + wp_links + wp_options + wp_postmeta + wp_posts + """ + + When I run `wp db tables --format=csv` + Then STDOUT should contain: + """ + ,wp_commentmeta,wp_comments, + """ + + When I run `wp db tables 'wp_post*' --format=csv` + Then STDOUT should be: + """ + wp_postmeta,wp_posts + """ + + @require-wp-3.9 @require-mysql-or-mariadb Scenario: List database tables on a multisite WordPress install Given a WP multisite install @@ -119,6 +155,86 @@ Feature: List database tables wp_posts """ + @require-sqlite + Scenario: List database tables on a multisite WordPress install + Given a WP multisite install + + When I run `wp db tables` + Then STDOUT should contain: + """ + _mysql_data_types_cache + wp_users + sqlite_sequence + wp_usermeta + wp_termmeta + wp_terms + wp_term_taxonomy + wp_term_relationships + wp_commentmeta + wp_comments + wp_links + wp_options + wp_postmeta + wp_posts + wp_blogs + wp_blogmeta + wp_registration_log + wp_site + wp_sitemeta + wp_signups + """ + + When I run `wp site create --slug=foo` + And I run `wp db tables --url=example.com/foo` + Then STDOUT should contain: + """ + wp_users + """ + And STDOUT should contain: + """ + wp_usermeta + """ + And STDOUT should contain: + """ + wp_2_posts + """ + + When I run `wp db tables --url=example.com/foo --scope=global` + Then STDOUT should not contain: + """ + wp_2_posts + """ + + When I run `wp db tables --all-tables-with-prefix` + Then STDOUT should contain: + """ + wp_2_posts + """ + And STDOUT should contain: + """ + wp_posts + """ + + When I run `wp db tables --url=example.com/foo --all-tables-with-prefix` + Then STDOUT should contain: + """ + wp_2_posts + """ + And STDOUT should not contain: + """ + wp_posts + """ + + When I run `wp db tables --url=example.com/foo --network` + Then STDOUT should contain: + """ + wp_2_posts + """ + And STDOUT should contain: + """ + wp_posts + """ + Scenario: Listing a site's tables should only list that site's tables Given a WP multisite install diff --git a/src/DB_Command.php b/src/DB_Command.php index 6f24b708..08b2a15a 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1176,9 +1176,12 @@ public function size( $args, $assoc_args ) { // Get the table size. if ( $is_sqlite ) { - // For SQLite, we cannot get individual table sizes easily. - // Just report 0 as a placeholder. - $table_bytes = 0; + $table_bytes = $wpdb->get_var( + $wpdb->prepare( + 'SELECT SUM(pgsize) as size_in_bytes FROM dbstat where name = %s LIMIT 1', + $table_name + ) + ); } else { $table_bytes = $wpdb->get_var( $wpdb->prepare( @@ -1840,7 +1843,12 @@ public function columns( $args, $assoc_args ) { ); $formatter_fields = [ 'Field', 'Type', 'Null', 'Key', 'Default', 'Extra' ]; - $formatter_args = [ + + if ( $this->is_sqlite() ) { + $formatter_fields = [ 'Field', 'Type', 'Null', 'Key', 'Default' ]; + } + + $formatter_args = [ 'format' => $format, ]; diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index d9d8b7dc..381f0c90 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -412,7 +412,8 @@ protected function sqlite_import( $file ) { } if ( '-' === $file ) { - $sql = stream_get_contents( STDIN ); + $sql = stream_get_contents( STDIN ); + $file = 'STDIN'; } else { if ( ! is_readable( $file ) ) { WP_CLI::error( sprintf( 'Import file missing or not readable: %s', $file ) ); From 3d5f055c84d5f1505df0ab539e18657221241248 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 24 Nov 2025 11:05:59 +0100 Subject: [PATCH 18/20] Correctly load sqlite drop-in --- src/DB_Command_SQLite.php | 60 +++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 381f0c90..e4fe0878 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -204,6 +204,46 @@ protected function sqlite_reset() { * @param string $query SQL query to execute. */ protected function sqlite_query( $query ) { + global $wpdb; + + // Use $wpdb if the SQLite drop-in is loaded. + if ( isset( $wpdb ) && $wpdb instanceof \WP_SQLite_DB ) { + try { + $is_row_modifying_query = preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE)\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 ) ) { + // No results to display. + return; + } + + // Display as a table similar to MySQL output. + $headers = array_keys( $results[0] ); + $this->display_table( $headers, $results ); + } + } catch ( Exception $e ) { + WP_CLI::error( 'Query failed: ' . $e->getMessage() ); + } + return; + } + + // Fallback to PDO if the drop-in is not loaded. $pdo = $this->get_sqlite_pdo(); if ( ! $pdo ) { @@ -499,14 +539,24 @@ protected function maybe_load_sqlite_dropin() { return; } + // Constants used in wp-includes/functions.php + 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' ) ) { - $wpinc = defined( 'WPINC' ) ? WPINC : 'wp-includes'; - $required_files = [ - ABSPATH . $wpinc . '/compat.php', - ABSPATH . $wpinc . '/plugin.php', - ABSPATH . $wpinc . '/class-wpdb.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 ) { From 3638ff2603691f41d2fbf6d8689068424998f23b Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 24 Nov 2025 11:09:03 +0100 Subject: [PATCH 19/20] Update PHPStan config --- tests/phpstan/scan-files.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/phpstan/scan-files.php b/tests/phpstan/scan-files.php index 80950306..92b1f46c 100644 --- a/tests/phpstan/scan-files.php +++ b/tests/phpstan/scan-files.php @@ -7,4 +7,7 @@ define( 'DB_PASSWORD', '' ); define( 'DB_CHARSET', '' ); define( 'DB_COLLATE', '' ); + + class WP_SQLite_DB extends \wpdb { + } } From 020b1d1dd08fbbad08da0b15474596327de55c85 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 24 Nov 2025 11:19:52 +0100 Subject: [PATCH 20/20] Fix another test --- features/db-columns.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/db-columns.feature b/features/db-columns.feature index 40f541e2..8372451f 100644 --- a/features/db-columns.feature +++ b/features/db-columns.feature @@ -60,5 +60,5 @@ Feature: Display information about a given table. When I try `wp db columns not_wp` Then STDOUT should be a table containing rows: | Field | Type | Null | Key | Default | - | date | date | NO | PRI | | - | awesome_stuff | text | YES | | | + | date | TEXT | NO | PRI | '' | + | awesome_stuff | TEXT | YES | | |