From 925732a84c8f770ff171d7d6a2e36c135f152f6f Mon Sep 17 00:00:00 2001 From: Rolf <62988563+rolf-yoast@users.noreply.github.com> Date: Fri, 19 Apr 2024 08:16:03 +0200 Subject: [PATCH 01/53] Add alignments to table --- src/DB_Command.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 37e3bae7..07ebcf8f 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -2,6 +2,7 @@ use WP_CLI\Formatter; use WP_CLI\Utils; +use cli\table\Column; /** * Performs basic database operations using credentials stored in wp-config.php. @@ -1145,7 +1146,6 @@ public function size( $args, $assoc_args ) { if ( ! empty( $size_format ) && ! $tables && ! $format && ! $human_readable && true !== $all_tables && true !== $all_tables_with_prefix ) { WP_CLI::line( str_replace( " {$size_format_display}", '', $rows[0]['Size'] ) ); } else { - // Sort the rows by user input if ( $orderby ) { usort( @@ -1166,7 +1166,8 @@ function ( $a, $b ) use ( $order, $orderby ) { // Display the rows. $args = [ - 'format' => $format, + 'format' => $format, + 'alignments' => [ 'Size' => Column::ALIGN_RIGHT ], ]; $formatter = new Formatter( $args, $fields ); From cc2972bd9339ee940dd322f017535c993fdd98d6 Mon Sep 17 00:00:00 2001 From: swissspidy Date: Tue, 11 Nov 2025 13:30:44 +0000 Subject: [PATCH 02/53] Update file(s) from wp-cli/.github --- AGENTS.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..1ff84f6d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,121 @@ +# Instructions + +This package is part of WP-CLI, the official command line interface for WordPress. For a detailed explanation of the project structure and development workflow, please refer to the main @README.md file. + +## Best Practices for Code Contributions + +When contributing to this package, please adhere to the following guidelines: + +* **Follow Existing Conventions:** Before writing any code, analyze the existing codebase in this package to understand the coding style, naming conventions, and architectural patterns. +* **Focus on the Package's Scope:** All changes should be relevant to the functionality of the package. +* **Write Tests:** All new features and bug fixes must be accompanied by acceptance tests using Behat. You can find the existing tests in the `features/` directory. There may be PHPUnit unit tests as well in the `tests/` directory. +* **Update Documentation:** If your changes affect the user-facing functionality, please update the relevant inline code documentation. + +### Building and running + +Before submitting any changes, it is crucial to validate them by running the full suite of static code analysis and tests. To run the full suite of checks, execute the following command: `composer test`. + +This single command ensures that your changes meet all the quality gates of the project. While you can run the individual steps separately, it is highly recommended to use this single command to ensure a comprehensive validation. + +### Useful Composer Commands + +The project uses Composer to manage dependencies and run scripts. The following commands are available: + +* `composer install`: Install dependencies. +* `composer test`: Run the full test suite, including linting, code style checks, static analysis, and unit/behavior tests. +* `composer lint`: Check for syntax errors. +* `composer phpcs`: Check for code style violations. +* `composer phpcbf`: Automatically fix code style violations. +* `composer phpstan`: Run static analysis. +* `composer phpunit`: Run unit tests. +* `composer behat`: Run behavior-driven tests. + +### Coding Style + +The project follows the `WP_CLI_CS` coding standard, which is enforced by PHP_CodeSniffer. The configuration can be found in `phpcs.xml.dist`. Before submitting any code, please run `composer phpcs` to check for violations and `composer phpcbf` to automatically fix them. + +## Documentation + +The `README.md` file might be generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). In that case, changes need to be made against the corresponding part of the codebase. + +### Inline Documentation + +Only write high-value comments if at all. Avoid talking to the user through comments. + +## Testing + +The project has a comprehensive test suite that includes unit tests, behavior-driven tests, and static analysis. + +* **Unit tests** are written with PHPUnit and can be found in the `tests/` directory. The configuration is in `phpunit.xml.dist`. +* **Behavior-driven tests** are written with Behat and can be found in the `features/` directory. The configuration is in `behat.yml`. +* **Static analysis** is performed with PHPStan. + +All tests are run on GitHub Actions for every pull request. + +When writing tests, aim to follow existing patterns. Key conventions include: + +* When adding tests, first examine existing tests to understand and conform to established conventions. +* For unit tests, extend the base `WP_CLI\Tests\TestCase` test class. +* For Behat tests, only WP-CLI commands installed in `composer.json` can be run. + +### Behat Steps + +WP-CLI makes use of a Behat-based testing framework and provides a set of custom step definitions to write feature tests. + +> **Note:** If you are expecting an error output in a test, you need to use `When I try ...` instead of `When I run ...` . + +#### Given + +* `Given an empty directory` - Creates an empty directory. +* `Given /^an? (empty|non-existent) ([^\s]+) directory$/` - Creates or deletes a specific directory. +* `Given an empty cache` - Clears the WP-CLI cache directory. +* `Given /^an? ([^\s]+) (file|cache file):$/` - Creates a file with the given contents. +* `Given /^"([^"]+)" replaced with "([^"]+)" in the ([^\s]+) file$/` - Search and replace a string in a file using regex. +* `Given /^that HTTP requests to (.*?) will respond with:$/` - Mock HTTP requests to a given URL. +* `Given WP files` - Download WordPress files without installing. +* `Given wp-config.php` - Create a wp-config.php file using `wp config create`. +* `Given a database` - Creates an empty database. +* `Given a WP install(ation)` - Installs WordPress. +* `Given a WP install(ation) in :subdir` - Installs WordPress in a given directory. +* `Given a WP install(ation) with Composer` - Installs WordPress with Composer. +* `Given a WP install(ation) with Composer and a custom vendor directory :vendor_directory` - Installs WordPress with Composer and a custom vendor directory. +* `Given /^a WP multisite (subdirectory|subdomain)?\s?(install|installation)$/` - Installs WordPress Multisite. +* `Given these installed and active plugins:` - Installs and activates one or more plugins. +* `Given a custom wp-content directory` - Configure a custom `wp-content` directory. +* `Given download:` - Download multiple files into the given destinations. +* `Given /^save (STDOUT|STDERR) ([\'].+[^\'])?\s?as \{(\w+)\}$/` - Store STDOUT or STDERR contents in a variable. +* `Given /^a new Phar with (?:the same version|version "([^"]+)")$/` - Build a new WP-CLI Phar file with a given version. +* `Given /^a downloaded Phar with (?:the same version|version "([^"]+)")$/` - Download a specific WP-CLI Phar version from GitHub. +* `Given /^save the (.+) file ([\'].+[^\'])? as \{(\w+)\}$/` - Stores the contents of the given file in a variable. +* `Given a misconfigured WP_CONTENT_DIR constant directory` - Modify wp-config.php to set `WP_CONTENT_DIR` to an empty string. +* `Given a dependency on current wp-cli` - Add `wp-cli/wp-cli` as a Composer dependency. +* `Given a PHP built-in web server` - Start a PHP built-in web server in the current directory. +* `Given a PHP built-in web server to serve :subdir` - Start a PHP built-in web server in the given subdirectory. + +#### When + +* ``When /^I launch in the background `([^`]+)`$/`` - Launch a given command in the background. +* ``When /^I (run|try) `([^`]+)`$/`` - Run or try a given command. +* ``When /^I (run|try) `([^`]+)` from '([^\s]+)'$/`` - Run or try a given command in a subdirectory. +* `When /^I (run|try) the previous command again$/` - Run or try the previous command again. + +#### Then + +* `Then /^the return code should( not)? be (\d+)$/` - Expect a specific exit code of the previous command. +* `Then /^(STDOUT|STDERR) should( strictly)? (be|contain|not contain):$/` - Check the contents of STDOUT or STDERR. +* `Then /^(STDOUT|STDERR) should be a number$/` - Expect STDOUT or STDERR to be a numeric value. +* `Then /^(STDOUT|STDERR) should not be a number$/` - Expect STDOUT or STDERR to not be a numeric value. +* `Then /^STDOUT should be a table containing rows:$/` - Expect STDOUT to be a table containing the given rows. +* `Then /^STDOUT should end with a table containing rows:$/` - Expect STDOUT to end with a table containing the given rows. +* `Then /^STDOUT should be JSON containing:$/` - Expect valid JSON output in STDOUT. +* `Then /^STDOUT should be a JSON array containing:$/` - Expect valid JSON array output in STDOUT. +* `Then /^STDOUT should be CSV containing:$/` - Expect STDOUT to be CSV containing certain values. +* `Then /^STDOUT should be YAML containing:$/` - Expect STDOUT to be YAML containing certain content. +* `Then /^(STDOUT|STDERR) should be empty$/` - Expect STDOUT or STDERR to be empty. +* `Then /^(STDOUT|STDERR) should not be empty$/` - Expect STDOUT or STDERR not to be empty. +* `Then /^(STDOUT|STDERR) should be a version string (<|<=|>|>=|==|=|<>) ([+\w.{}-]+)$/` - Expect STDOUT or STDERR to be a version string comparing to the given version. +* `Then /^the (.+) (file|directory) should( strictly)? (exist|not exist|be:|contain:|not contain):$/` - Expect a certain file or directory to (not) exist or (not) contain certain contents. +* `Then /^the contents of the (.+) file should( not)? match (((\/.*\/)|(#.#))([a-z]+)?)$/` - Match file contents against a regex. +* `Then /^(STDOUT|STDERR) should( not)? match (((\/.*\/)|(#.#))([a-z]+)?)$/` - Match STDOUT or STDERR against a regex. +* `Then /^an email should (be sent|not be sent)$/` - Expect an email to be sent (or not). +* `Then the HTTP status code should be :code` - Expect the HTTP status code for visiting `http://localhost:8080`. From 2f7e8c36781c9863fae2d5e888386d3e8d7ee8af Mon Sep 17 00:00:00 2001 From: swissspidy Date: Thu, 11 Dec 2025 13:02:37 +0000 Subject: [PATCH 03/53] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/copilot-setup-steps.yml diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000..bf9327a9 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,46 @@ +name: "Copilot Setup Steps" + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Check existence of composer.json file + id: check_composer_file + uses: andstor/file-existence-action@v3 + with: + files: "composer.json" + + - name: Set up PHP environment + if: steps.check_composer_file.outputs.files_exists == 'true' + uses: shivammathur/setup-php@v2 + with: + php-version: 'latest' + ini-values: zend.assertions=1, error_reporting=-1, display_errors=On + coverage: 'none' + tools: composer,cs2pr + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Composer dependencies & cache dependencies + if: steps.check_composer_file.outputs.files_exists == 'true' + uses: ramsey/composer-install@v3 + env: + COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} + with: + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") From 7c7e1c8a40afc569aaf729afa7b816f39c6fec20 Mon Sep 17 00:00:00 2001 From: swissspidy Date: Thu, 11 Dec 2025 18:23:38 +0000 Subject: [PATCH 04/53] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index bf9327a9..5158ca68 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Check existence of composer.json file id: check_composer_file From 3b82084e98078ea71def8e13eb96efe133384358 Mon Sep 17 00:00:00 2001 From: swissspidy Date: Fri, 12 Dec 2025 11:38:32 +0000 Subject: [PATCH 05/53] Update file(s) from wp-cli/.github --- .github/workflows/manage-labels.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/manage-labels.yml diff --git a/.github/workflows/manage-labels.yml b/.github/workflows/manage-labels.yml new file mode 100644 index 00000000..45711bde --- /dev/null +++ b/.github/workflows/manage-labels.yml @@ -0,0 +1,19 @@ +--- +name: Manage Labels + +'on': + workflow_dispatch: + push: + branches: + - main + - master + paths: + - 'composer.json' + +permissions: + issues: write + contents: read + +jobs: + manage-labels: + uses: wp-cli/.github/.github/workflows/reusable-manage-labels.yml@main From 2a36d5e64c9a5f5a793193f970218a1d35456373 Mon Sep 17 00:00:00 2001 From: schlessera Date: Fri, 12 Dec 2025 12:30:01 +0000 Subject: [PATCH 06/53] Update file(s) from wp-cli/.github --- .github/workflows/check-branch-alias.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/check-branch-alias.yml diff --git a/.github/workflows/check-branch-alias.yml b/.github/workflows/check-branch-alias.yml new file mode 100644 index 00000000..17a7c490 --- /dev/null +++ b/.github/workflows/check-branch-alias.yml @@ -0,0 +1,12 @@ +name: Check Branch Alias + +on: + release: + types: [released] + workflow_dispatch: + +permissions: {} + +jobs: + check-branch-alias: + uses: wp-cli/.github/.github/workflows/reusable-check-branch-alias.yml@main From 3a3f0286d20467afad3f03aa9f1a789bb33ccca6 Mon Sep 17 00:00:00 2001 From: schlessera Date: Fri, 12 Dec 2025 12:46:33 +0000 Subject: [PATCH 07/53] Update file(s) from wp-cli/.github --- .github/workflows/issue-triage.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/issue-triage.yml diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml new file mode 100644 index 00000000..634607e6 --- /dev/null +++ b/.github/workflows/issue-triage.yml @@ -0,0 +1,18 @@ +--- +name: Issue Triage + +'on': + issues: + types: [opened] + workflow_dispatch: + inputs: + issue_number: + description: 'Issue number to triage (leave empty to process all)' + required: false + type: string + +jobs: + issue-triage: + uses: wp-cli/.github/.github/workflows/reusable-issue-triage.yml@main + with: + issue_number: ${{ github.event_name == 'workflow_dispatch' && inputs.issue_number || github.event.issue.number }} From c39048f5bb3e4137839b3d2b102191d50976d263 Mon Sep 17 00:00:00 2001 From: swissspidy Date: Wed, 17 Dec 2025 15:55:12 +0000 Subject: [PATCH 08/53] Update file(s) from wp-cli/.github --- .github/workflows/check-branch-alias.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-branch-alias.yml b/.github/workflows/check-branch-alias.yml index 17a7c490..78da6371 100644 --- a/.github/workflows/check-branch-alias.yml +++ b/.github/workflows/check-branch-alias.yml @@ -5,7 +5,9 @@ on: types: [released] workflow_dispatch: -permissions: {} +permissions: + contents: write + pull-requests: write jobs: check-branch-alias: From 8cf22722478e18b493ed8bb818c7d9beb658165d Mon Sep 17 00:00:00 2001 From: swissspidy Date: Sat, 20 Dec 2025 21:58:02 +0000 Subject: [PATCH 09/53] Update file(s) from wp-cli/.github --- .github/workflows/issue-triage.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index 634607e6..cfd68e10 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -1,13 +1,15 @@ --- -name: Issue Triage +name: Issue and PR Triage 'on': issues: types: [opened] + pull_request: + types: [opened] workflow_dispatch: inputs: issue_number: - description: 'Issue number to triage (leave empty to process all)' + description: 'Issue/PR number to triage (leave empty to process all)' required: false type: string @@ -15,4 +17,10 @@ jobs: issue-triage: uses: wp-cli/.github/.github/workflows/reusable-issue-triage.yml@main with: - issue_number: ${{ github.event_name == 'workflow_dispatch' && inputs.issue_number || github.event.issue.number }} + issue_number: >- + ${{ + (github.event_name == 'workflow_dispatch' && inputs.issue_number) || + (github.event_name == 'pull_request' && github.event.pull_request.number) || + (github.event_name == 'issues' && github.event.issue.number) || + '' + }} From ae615235cafd0dcdd65eaf2b840e7fe857c4c566 Mon Sep 17 00:00:00 2001 From: swissspidy Date: Tue, 6 Jan 2026 14:03:52 +0000 Subject: [PATCH 10/53] Update file(s) from wp-cli/.github --- .github/workflows/issue-triage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index cfd68e10..14dffc54 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -4,7 +4,7 @@ name: Issue and PR Triage 'on': issues: types: [opened] - pull_request: + pull_request_target: types: [opened] workflow_dispatch: inputs: @@ -20,7 +20,7 @@ jobs: issue_number: >- ${{ (github.event_name == 'workflow_dispatch' && inputs.issue_number) || - (github.event_name == 'pull_request' && github.event.pull_request.number) || + (github.event_name == 'pull_request_target' && github.event.pull_request.number) || (github.event_name == 'issues' && github.event.issue.number) || '' }} From bfb8ece6360ffd2057ea1a053e2b907f27af7b1e Mon Sep 17 00:00:00 2001 From: swissspidy Date: Tue, 6 Jan 2026 14:36:07 +0000 Subject: [PATCH 11/53] Update file(s) from wp-cli/.github --- .github/workflows/welcome-new-contributors.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/welcome-new-contributors.yml diff --git a/.github/workflows/welcome-new-contributors.yml b/.github/workflows/welcome-new-contributors.yml new file mode 100644 index 00000000..c38e033b --- /dev/null +++ b/.github/workflows/welcome-new-contributors.yml @@ -0,0 +1,12 @@ +name: Welcome New Contributors + +on: + pull_request_target: + types: [opened] + branches: + - main + - master + +jobs: + welcome: + uses: wp-cli/.github/.github/workflows/reusable-welcome-new-contributors.yml@main From 237d08fe14277df7392c706033faffb2140b9d99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:21:43 +0000 Subject: [PATCH 12/53] Initial plan From 0814b9cb6fe8d1a8f0b88ea9e7b019121bb02f5e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:25:46 +0000 Subject: [PATCH 13/53] Add --quiet support for db check, optimize, and repair commands Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/db-quiet.feature | 109 ++++++++++++++++++++++++++++++++++++++ src/DB_Command.php | 18 +++++++ 2 files changed, 127 insertions(+) create mode 100644 features/db-quiet.feature diff --git a/features/db-quiet.feature b/features/db-quiet.feature new file mode 100644 index 00000000..03a8212a --- /dev/null +++ b/features/db-quiet.feature @@ -0,0 +1,109 @@ +Feature: Quiet mode for database operations + + Scenario: db check with --quiet flag should only show errors + Given a WP install + + When I run `wp db check --quiet` + Then STDOUT should not contain: + """ + wp_cli_test.wp_users + """ + And STDOUT should be empty + + Scenario: db optimize with --quiet flag should only show errors + Given a WP install + + When I run `wp db optimize --quiet` + Then STDOUT should not contain: + """ + wp_cli_test.wp_users + """ + And STDOUT should be empty + + Scenario: db repair with --quiet flag should only show errors + Given a WP install + + When I run `wp db repair --quiet` + Then STDOUT should not contain: + """ + wp_cli_test.wp_users + """ + And STDOUT should be empty + + Scenario: db check without --quiet flag should show informational messages + Given a WP install + + When I run `wp db check` + Then STDOUT should contain: + """ + wp_cli_test.wp_users + """ + And STDOUT should contain: + """ + Success: Database checked. + """ + + Scenario: db optimize without --quiet flag should show informational messages + Given a WP install + + When I run `wp db optimize` + Then STDOUT should contain: + """ + wp_cli_test.wp_users + """ + And STDOUT should contain: + """ + Success: Database optimized. + """ + + Scenario: db repair without --quiet flag should show informational messages + Given a WP install + + When I run `wp db repair` + Then STDOUT should contain: + """ + wp_cli_test.wp_users + """ + And STDOUT should contain: + """ + Success: Database repaired. + """ + + Scenario: db check can explicitly pass --silent to mysqlcheck + Given a WP install + + When I run `wp db check --silent` + Then STDOUT should not contain: + """ + wp_cli_test.wp_users + """ + And STDOUT should contain: + """ + Success: Database checked. + """ + + Scenario: db optimize can explicitly pass --silent to mysqlcheck + Given a WP install + + When I run `wp db optimize --silent` + Then STDOUT should not contain: + """ + wp_cli_test.wp_users + """ + And STDOUT should contain: + """ + Success: Database optimized. + """ + + Scenario: db repair can explicitly pass --silent to mysqlcheck + Given a WP install + + When I run `wp db repair --silent` + Then STDOUT should not contain: + """ + wp_cli_test.wp_users + """ + And STDOUT should contain: + """ + Success: Database repaired. + """ diff --git a/src/DB_Command.php b/src/DB_Command.php index 06781cee..222b4f6d 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -259,6 +259,12 @@ public function check( $_, $assoc_args ) { WP_CLI::debug( "Running shell command: {$command}", 'db' ); $assoc_args['check'] = true; + + // Pass --silent to mysqlcheck when in quiet mode. + if ( WP_CLI::get_config( 'quiet' ) ) { + $assoc_args['silent'] = true; + } + self::run( Utils\esc_cmd( $command, DB_NAME ), $assoc_args @@ -307,6 +313,12 @@ public function optimize( $_, $assoc_args ) { WP_CLI::debug( "Running shell command: {$command}", 'db' ); $assoc_args['optimize'] = true; + + // Pass --silent to mysqlcheck when in quiet mode. + if ( WP_CLI::get_config( 'quiet' ) ) { + $assoc_args['silent'] = true; + } + self::run( Utils\esc_cmd( $command, DB_NAME ), $assoc_args @@ -355,6 +367,12 @@ public function repair( $_, $assoc_args ) { WP_CLI::debug( "Running shell command: {$command}", 'db' ); $assoc_args['repair'] = true; + + // Pass --silent to mysqlcheck when in quiet mode. + if ( WP_CLI::get_config( 'quiet' ) ) { + $assoc_args['silent'] = true; + } + self::run( Utils\esc_cmd( $command, DB_NAME ), $assoc_args From 73aad752d1bba8cdee908be826d1f7b1497e244d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 13 Jan 2026 10:18:53 +0100 Subject: [PATCH 14/53] Move tests --- features/db-check.feature | 19 +++++++ features/db-quiet.feature | 109 -------------------------------------- features/db.feature | 47 +++++++++++++++- 3 files changed, 65 insertions(+), 110 deletions(-) delete mode 100644 features/db-quiet.feature diff --git a/features/db-check.feature b/features/db-check.feature index aa5ce47b..7c105c82 100644 --- a/features/db-check.feature +++ b/features/db-check.feature @@ -13,6 +13,25 @@ Feature: Check the database Success: Database checked. """ + Scenario: db check with --quiet flag should only show errors + Given a WP install + + When I run `wp db check --quiet` + Then STDOUT should be empty + + Scenario: db check can explicitly pass --silent to mysqlcheck + Given a WP install + + When I run `wp db check --silent` + Then STDOUT should not contain: + """ + wp_cli_test.wp_users + """ + And STDOUT should contain: + """ + Success: Database checked. + """ + Scenario: Run db check with MySQL defaults to check the database Given a WP install diff --git a/features/db-quiet.feature b/features/db-quiet.feature deleted file mode 100644 index 03a8212a..00000000 --- a/features/db-quiet.feature +++ /dev/null @@ -1,109 +0,0 @@ -Feature: Quiet mode for database operations - - Scenario: db check with --quiet flag should only show errors - Given a WP install - - When I run `wp db check --quiet` - Then STDOUT should not contain: - """ - wp_cli_test.wp_users - """ - And STDOUT should be empty - - Scenario: db optimize with --quiet flag should only show errors - Given a WP install - - When I run `wp db optimize --quiet` - Then STDOUT should not contain: - """ - wp_cli_test.wp_users - """ - And STDOUT should be empty - - Scenario: db repair with --quiet flag should only show errors - Given a WP install - - When I run `wp db repair --quiet` - Then STDOUT should not contain: - """ - wp_cli_test.wp_users - """ - And STDOUT should be empty - - Scenario: db check without --quiet flag should show informational messages - Given a WP install - - When I run `wp db check` - Then STDOUT should contain: - """ - wp_cli_test.wp_users - """ - And STDOUT should contain: - """ - Success: Database checked. - """ - - Scenario: db optimize without --quiet flag should show informational messages - Given a WP install - - When I run `wp db optimize` - Then STDOUT should contain: - """ - wp_cli_test.wp_users - """ - And STDOUT should contain: - """ - Success: Database optimized. - """ - - Scenario: db repair without --quiet flag should show informational messages - Given a WP install - - When I run `wp db repair` - Then STDOUT should contain: - """ - wp_cli_test.wp_users - """ - And STDOUT should contain: - """ - Success: Database repaired. - """ - - Scenario: db check can explicitly pass --silent to mysqlcheck - Given a WP install - - When I run `wp db check --silent` - Then STDOUT should not contain: - """ - wp_cli_test.wp_users - """ - And STDOUT should contain: - """ - Success: Database checked. - """ - - Scenario: db optimize can explicitly pass --silent to mysqlcheck - Given a WP install - - When I run `wp db optimize --silent` - Then STDOUT should not contain: - """ - wp_cli_test.wp_users - """ - And STDOUT should contain: - """ - Success: Database optimized. - """ - - Scenario: db repair can explicitly pass --silent to mysqlcheck - Given a WP install - - When I run `wp db repair --silent` - Then STDOUT should not contain: - """ - wp_cli_test.wp_users - """ - And STDOUT should contain: - """ - Success: Database repaired. - """ diff --git a/features/db.feature b/features/db.feature index dbfdb4b0..0a2be953 100644 --- a/features/db.feature +++ b/features/db.feature @@ -185,6 +185,51 @@ Feature: Perform database operations """ And STDOUT should not be empty + Scenario: db optimize with --quiet flag should only show errors + Given a WP install + + When I run `wp db optimize --quiet` + Then STDOUT should not contain: + """ + error + """ + + + Scenario: db optimize can explicitly pass --silent to mysqlcheck + Given a WP install + + When I run `wp db optimize --silent` + Then STDOUT should not contain: + """ + wp_cli_test.wp_users + """ + And STDOUT should contain: + """ + Success: Database optimized. + """ + + Scenario: db repair with --quiet flag should only show errors + Given a WP install + + When I run `wp db repair --quiet` + Then STDOUT should not contain: + """ + error + """ + + Scenario: db repair can explicitly pass --silent to mysqlcheck + Given a WP install + + When I run `wp db repair --silent` + Then STDOUT should not contain: + """ + error + """ + And STDOUT should contain: + """ + Success: Database repaired. + """ + Scenario: DB Query Given a WP install @@ -334,7 +379,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: """ From 1a36babaac120433d92eb5dbe3c0cbf1ef956141 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 13 Jan 2026 12:36:54 +0100 Subject: [PATCH 15/53] Update test --- features/db.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/db.feature b/features/db.feature index 0a2be953..1f44a6c5 100644 --- a/features/db.feature +++ b/features/db.feature @@ -201,7 +201,7 @@ Feature: Perform database operations When I run `wp db optimize --silent` Then STDOUT should not contain: """ - wp_cli_test.wp_users + error """ And STDOUT should contain: """ From 9780f6791ce5727118a4eb4bbedfdd8b94fcf76c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 16 Jan 2026 23:03:00 +0100 Subject: [PATCH 16/53] Remove tests --- features/db.feature | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/features/db.feature b/features/db.feature index 1f44a6c5..26b3d5f8 100644 --- a/features/db.feature +++ b/features/db.feature @@ -185,29 +185,6 @@ Feature: Perform database operations """ And STDOUT should not be empty - Scenario: db optimize with --quiet flag should only show errors - Given a WP install - - When I run `wp db optimize --quiet` - Then STDOUT should not contain: - """ - error - """ - - - Scenario: db optimize can explicitly pass --silent to mysqlcheck - Given a WP install - - When I run `wp db optimize --silent` - Then STDOUT should not contain: - """ - error - """ - And STDOUT should contain: - """ - Success: Database optimized. - """ - Scenario: db repair with --quiet flag should only show errors Given a WP install From aee6570e6ffeb66de128116aed2d3debf459fb68 Mon Sep 17 00:00:00 2001 From: swissspidy Date: Tue, 20 Jan 2026 13:08:27 +0000 Subject: [PATCH 17/53] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 8 ++++---- .github/workflows/issue-triage.yml | 7 +++++++ .github/workflows/regenerate-readme.yml | 4 ++++ .github/workflows/welcome-new-contributors.yml | 3 +++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 5158ca68..44bdaa0b 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -17,17 +17,17 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Check existence of composer.json file id: check_composer_file - uses: andstor/file-existence-action@v3 + uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3 with: files: "composer.json" - name: Set up PHP environment if: steps.check_composer_file.outputs.files_exists == 'true' - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2 with: php-version: 'latest' ini-values: zend.assertions=1, error_reporting=-1, display_errors=On @@ -38,7 +38,7 @@ jobs: - name: Install Composer dependencies & cache dependencies if: steps.check_composer_file.outputs.files_exists == 'true' - uses: ramsey/composer-install@v3 + uses: ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520 # v3 env: COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} with: diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index 14dffc54..68334703 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -13,6 +13,13 @@ name: Issue and PR Triage required: false type: string +permissions: + issues: write + pull-requests: write + actions: write + contents: read + models: read + jobs: issue-triage: uses: wp-cli/.github/.github/workflows/reusable-issue-triage.yml@main diff --git a/.github/workflows/regenerate-readme.yml b/.github/workflows/regenerate-readme.yml index c633d9d4..6198d630 100644 --- a/.github/workflows/regenerate-readme.yml +++ b/.github/workflows/regenerate-readme.yml @@ -10,6 +10,10 @@ on: - "features/**" - "README.md" +permissions: + contents: write + pull-requests: write + jobs: regenerate-readme: uses: wp-cli/.github/.github/workflows/reusable-regenerate-readme.yml@main diff --git a/.github/workflows/welcome-new-contributors.yml b/.github/workflows/welcome-new-contributors.yml index c38e033b..bc01490b 100644 --- a/.github/workflows/welcome-new-contributors.yml +++ b/.github/workflows/welcome-new-contributors.yml @@ -7,6 +7,9 @@ on: - main - master +permissions: + pull-requests: write + jobs: welcome: uses: wp-cli/.github/.github/workflows/reusable-welcome-new-contributors.yml@main From e0d482c25214fe19c27a619e5962b95cc6c59cf7 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 21 Jan 2026 13:28:14 +0100 Subject: [PATCH 18/53] spellcheck fixes --- .typos.toml | 6 ++++++ src/DB_Command.php | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .typos.toml diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 00000000..965f891f --- /dev/null +++ b/.typos.toml @@ -0,0 +1,6 @@ +[default] +extend-ignore-re = [ + "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", + "(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on", + "(#|//)\\s*spellchecker:ignore-next-line\\n.*" +] diff --git a/src/DB_Command.php b/src/DB_Command.php index 9807b51b..6a077424 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1218,6 +1218,7 @@ function ( $a, $b ) use ( $order, $orderby ) { ); } + die('ddd'); // Display the rows. $args = [ 'format' => $format, @@ -1817,7 +1818,7 @@ private static function run( $cmd, $assoc_args = [], $send_to_shell = true, $int $required['default-character-set'] = constant( 'DB_CHARSET' ); } - // Using 'dbuser' as option name to workaround clash with WP-CLI's global WP 'user' parameter, with 'dbpass' also available for tidyness. + // Using 'dbuser' as option name to workaround clash with WP-CLI's global WP 'user' parameter, with 'dbpass' also available for tidiness. if ( isset( $assoc_args['dbuser'] ) ) { $required['user'] = $assoc_args['dbuser']; unset( $assoc_args['dbuser'] ); From fa7f6d16ea08619ad4a04caf4953963f217edfdb Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 21 Jan 2026 13:39:13 +0100 Subject: [PATCH 19/53] Revert accidental change --- src/DB_Command.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 6a077424..77676172 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1218,7 +1218,6 @@ function ( $a, $b ) use ( $order, $orderby ) { ); } - die('ddd'); // Display the rows. $args = [ 'format' => $format, From 51732e81c4d8b125ce5809dad01b44bc8c354c57 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 21 Jan 2026 13:41:25 +0100 Subject: [PATCH 20/53] Add test --- features/db-size.feature | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/features/db-size.feature b/features/db-size.feature index 0958bbf1..4ff93c04 100644 --- a/features/db-size.feature +++ b/features/db-size.feature @@ -243,3 +243,30 @@ Feature: Display database size """ wp_posts """ + + @require-mysql + Scenario: Display table size with right alignment + Given a WP install + + When I run `SHELL_PIPE=0 wp db size --tables --all-tables --orderby=size --order=desc --size_format=mb` + Then STDOUT should strictly be: + """ + +-------------------------+------+ + | Name | Size | + +-------------------------+------+ + | _mysql_data_types_cache | 0 MB | + | wp_users | 0 MB | + | sqlite_sequence | 0 MB | + | wp_usermeta | 0 MB | + | wp_termmeta | 0 MB | + | wp_terms | 0 MB | + | wp_term_taxonomy | 0 MB | + | wp_term_relationships | 0 MB | + | wp_commentmeta | 0 MB | + | wp_comments | 0 MB | + | wp_links | 0 MB | + | wp_options | 0 MB | + | wp_postmeta | 0 MB | + | wp_posts | 0 MB | + +-------------------------+------+ + """ From 06ef2e4f6aa68c2eb3247d76ae7b7e757a9e8adb Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 21 Jan 2026 14:27:15 +0100 Subject: [PATCH 21/53] Update test --- features/db-size.feature | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/db-size.feature b/features/db-size.feature index 4ff93c04..c1cba7cb 100644 --- a/features/db-size.feature +++ b/features/db-size.feature @@ -247,8 +247,10 @@ Feature: Display database size @require-mysql Scenario: Display table size with right alignment Given a WP install + And I run `wp site empty --yes` + And I run `wp post generate --post_type=page --post_status=draft --count=100` - When I run `SHELL_PIPE=0 wp db size --tables --all-tables --orderby=size --order=desc --size_format=mb` + When I run `SHELL_PIPE=0 wp db size --tables --all-tables --orderby=size --order=desc --size_format=kb` Then STDOUT should strictly be: """ +-------------------------+------+ From e83917ef8ed927b0e12f7eebcf4037d60366d836 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 21 Jan 2026 14:46:03 +0100 Subject: [PATCH 22/53] Revert test --- features/db-size.feature | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/features/db-size.feature b/features/db-size.feature index c1cba7cb..0958bbf1 100644 --- a/features/db-size.feature +++ b/features/db-size.feature @@ -243,32 +243,3 @@ Feature: Display database size """ wp_posts """ - - @require-mysql - Scenario: Display table size with right alignment - Given a WP install - And I run `wp site empty --yes` - And I run `wp post generate --post_type=page --post_status=draft --count=100` - - When I run `SHELL_PIPE=0 wp db size --tables --all-tables --orderby=size --order=desc --size_format=kb` - Then STDOUT should strictly be: - """ - +-------------------------+------+ - | Name | Size | - +-------------------------+------+ - | _mysql_data_types_cache | 0 MB | - | wp_users | 0 MB | - | sqlite_sequence | 0 MB | - | wp_usermeta | 0 MB | - | wp_termmeta | 0 MB | - | wp_terms | 0 MB | - | wp_term_taxonomy | 0 MB | - | wp_term_relationships | 0 MB | - | wp_commentmeta | 0 MB | - | wp_comments | 0 MB | - | wp_links | 0 MB | - | wp_options | 0 MB | - | wp_postmeta | 0 MB | - | wp_posts | 0 MB | - +-------------------------+------+ - """ From 5077d19a979f026b980d4559ae79e98cb66f5442 Mon Sep 17 00:00:00 2001 From: ernilambar Date: Thu, 22 Jan 2026 17:20:48 +0000 Subject: [PATCH 23/53] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 44bdaa0b..a48b8d2c 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Check existence of composer.json file id: check_composer_file From e4cb95fe045960f09d543b59bf4d2d44696f7442 Mon Sep 17 00:00:00 2001 From: Alain Schlesser Date: Thu, 22 Jan 2026 17:21:30 +0000 Subject: [PATCH 24/53] Regenerate README file - 2026-01-22 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 547fa52a..95b2df1d 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,8 @@ Opens a MySQL console using credentials from wp-config.php wp db cli [--database=] [--default-character-set=] [--dbuser=] [--dbpass=] [--=] [--defaults] ~~~ +**Alias:** `connect` + **OPTIONS** [--database=] @@ -432,6 +434,8 @@ Exports the database to a file or to STDOUT. wp db export [] [--dbuser=] [--dbpass=] [--=] [--tables=] [--exclude_tables=] [--include-tablespaces] [--porcelain] [--add-drop-table] [--defaults] ~~~ +**Alias:** `dump` + Runs `mysqldump` utility using `DB_HOST`, `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials specified in wp-config.php. Accepts any valid `mysqldump` flags. From 9b69699f2b42b6b2a6056093127d921633db3a92 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 12 Feb 2026 13:26:03 +0100 Subject: [PATCH 25/53] Add schedule for code quality workflow --- .github/workflows/code-quality.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 07e4fd1f..e9fe5776 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -6,6 +6,8 @@ on: branches: - main - master + schedule: + - cron: '17 2 * * *' # Run every day on a seemly random time. jobs: code-quality: From a959fd36ca469642743b7654e812b2cb87b9ab7e Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 13 Feb 2026 15:25:07 +0100 Subject: [PATCH 26/53] Fix newly reported PHPStan errors --- src/DB_Command.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/DB_Command.php b/src/DB_Command.php index f3acbdac..7409935c 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1161,6 +1161,12 @@ 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_key = (int) $size_key; + $size_format = isset( $sizes[ $size_key ] ) ? $sizes[ $size_key ] : $sizes[0]; } From f96946acfac6e21f1b6654e5fc26520da13026ff Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 7 Mar 2026 15:54:29 +0000 Subject: [PATCH 27/53] Fix REPLACE() string function being misidentified as write query Use negative lookahead REPLACE(?!\s*\() instead of bare REPLACE to distinguish the MySQL REPLACE() string function from the REPLACE INTO (or bare REPLACE) DML write statement. This avoids a false positive where SELECT queries containing REPLACE() are routed through the write-query code path, causing their results to be silently swallowed. Fixes wp-cli/db-command#313 --- src/DB_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 7409935c..245a10cd 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -538,7 +538,7 @@ public function query( $args, $assoc_args ) { $assoc_args['execute'] = $this->get_sql_mode_query( $assoc_args ) . $assoc_args['execute']; } - $is_row_modifying_query = isset( $assoc_args['execute'] ) && preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE|LOAD DATA)\b/i', $assoc_args['execute'] ); + $is_row_modifying_query = isset( $assoc_args['execute'] ) && preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE(?!\s*\()|LOAD DATA)\b/i', $assoc_args['execute'] ); if ( $is_row_modifying_query ) { $assoc_args['execute'] .= '; SELECT ROW_COUNT();'; From 9f32ef9d38be2a4685e837c4192bb381d0226cf6 Mon Sep 17 00:00:00 2001 From: swissspidy Date: Tue, 10 Mar 2026 04:11:16 +0000 Subject: [PATCH 28/53] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index a48b8d2c..42d610ad 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -38,7 +38,7 @@ jobs: - name: Install Composer dependencies & cache dependencies if: steps.check_composer_file.outputs.files_exists == 'true' - uses: ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520 # v3 + uses: ramsey/composer-install@a35c6ebd3d08125aaf8852dff361e686a1a67947 # v3 env: COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} with: From 67f02b80316609225289c6de3016176ec8c9ff68 Mon Sep 17 00:00:00 2001 From: swissspidy Date: Thu, 12 Mar 2026 07:26:19 +0000 Subject: [PATCH 29/53] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 42d610ad..a6bb2732 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,9 +21,7 @@ jobs: - name: Check existence of composer.json file id: check_composer_file - uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3 - with: - files: "composer.json" + run: echo "files_exists=$(test -f composer.json && echo true || echo false)" >> "$GITHUB_OUTPUT" - name: Set up PHP environment if: steps.check_composer_file.outputs.files_exists == 'true' From dbfe4c6f248f73e5cca31d802c3d4b8952ddcfca Mon Sep 17 00:00:00 2001 From: swissspidy Date: Sun, 15 Mar 2026 17:24:33 +0000 Subject: [PATCH 30/53] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index a6bb2732..80ebcb00 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -25,7 +25,7 @@ jobs: - name: Set up PHP environment if: steps.check_composer_file.outputs.files_exists == 'true' - uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2 + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2 with: php-version: 'latest' ini-values: zend.assertions=1, error_reporting=-1, display_errors=On From ab1a39acc9895d0154d87d6717962a15173b3274 Mon Sep 17 00:00:00 2001 From: swissspidy Date: Mon, 16 Mar 2026 07:03:42 +0000 Subject: [PATCH 31/53] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 80ebcb00..32404820 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -36,7 +36,7 @@ jobs: - name: Install Composer dependencies & cache dependencies if: steps.check_composer_file.outputs.files_exists == 'true' - uses: ramsey/composer-install@a35c6ebd3d08125aaf8852dff361e686a1a67947 # v3 + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v3 env: COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} with: From 089afe8ed9176746e08d3562952eb77386fba1b6 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 16 Mar 2026 16:14:47 +0100 Subject: [PATCH 32/53] Add .gitattributes file See wp-cli/wp-cli#5070 --- .gitattributes | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..d84f4ade --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +/.actrc export-ignore +/.distignore export-ignore +/.editorconfig export-ignore +/.github export-ignore +/.gitignore export-ignore +/.typos.toml export-ignore +/AGENTS.md export-ignore +/behat.yml export-ignore +/features export-ignore +/phpcs.xml.dist export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore +/wp-cli.yml export-ignore From 358e2a0d800b8cf6b07c9fe81e249ef44d3d3799 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:55:05 +0100 Subject: [PATCH 33/53] Add SQLite compatibility to wp db commands (#299) Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Pascal Birchler --- features/db-check.feature | 16 + features/db-cli.feature | 11 + features/db-columns.feature | 14 +- features/db-create.feature | 24 ++ features/db-export.feature | 74 ++++- features/db-import.feature | 52 +++- features/db-optimize.feature | 21 ++ features/db-query.feature | 7 + features/db-repair.feature | 21 ++ features/db-search.feature | 65 ++++- features/db-size.feature | 54 ++++ features/db-tables.feature | 130 ++++++++- features/db.feature | 93 +++++- phpcs.xml.dist | 3 + src/DB_Command.php | 145 ++++++++-- src/DB_Command_SQLite.php | 545 +++++++++++++++++++++++++++++++++++ tests/phpstan/scan-files.php | 3 + 17 files changed, 1240 insertions(+), 38 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 create mode 100644 src/DB_Command_SQLite.php diff --git a/features/db-check.feature b/features/db-check.feature index 7c105c82..523ddd99 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,12 +14,14 @@ Feature: Check the database Success: Database checked. """ + @require-mysql-or-mariadb Scenario: db check with --quiet flag should only show errors Given a WP install When I run `wp db check --quiet` Then STDOUT should be empty + @require-mysql-or-mariadb Scenario: db check can explicitly pass --silent to mysqlcheck Given a WP install @@ -32,6 +35,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 @@ -45,6 +49,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 @@ -58,6 +63,7 @@ Feature: Check the database Success: Database checked. """ + @require-mysql-or-mariadb Scenario: Run db check with passed-in options Given a WP install @@ -143,6 +149,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 @@ -155,3 +162,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 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 new file mode 100644 index 00000000..4ef8b885 --- /dev/null +++ b/features/db-cli.feature @@ -0,0 +1,11 @@ +Feature: Open a MySQL console + + @require-sqlite + Scenario: SQLite commands that show warnings for cli + Given a WP install + + 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-columns.feature b/features/db-columns.feature index b2e1ee76..ba55cf6b 100644 --- a/features/db-columns.feature +++ b/features/db-columns.feature @@ -1,7 +1,7 @@ Feature: Display information about a given table. # This requires conditional tags to target different DB versions, as bigint(20) is only bigint on MySQL 8. - @require-wp-4.2 @broken + @broken Scenario: Display information about the wp_posts table Given a WP install @@ -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 | TEXT | NO | PRI | '' | + | awesome_stuff | TEXT | YES | | | 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-export.feature b/features/db-export.feature index 370ae1df..7635ffa9 100644 --- a/features/db-export.feature +++ b/features/db-export.feature @@ -20,20 +20,57 @@ Feature: Export a WordPress database """ And the wp_cli_test.sql file should exist + @skip-sqlite 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: - """ - wp_users - """ - And the wp_cli_test.sql file should contain: + 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["`]?/ + + @skip-sqlite + Scenario: Include only specific tables when exporting the database + Given a WP install + + When I try `wp db export wp_cli_test.sql --tables=wp_users --porcelain` + Then the wp_cli_test.sql file should exist + And the contents of the wp_cli_test.sql file should match /CREATE TABLE ["`]?wp_users["`]?/ + And the contents of the wp_cli_test.sql file should not match /CREATE TABLE ["`]?wp_posts["`]?/ + And the contents of the wp_cli_test.sql file should not match /CREATE TABLE ["`]?wp_options["`]?/ + + @require-sqlite + Scenario: Exclude tables when exporting the database + Given a WP install + + 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 /_mysql_data_types_cache/ + 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["`]?/ + + @require-sqlite + Scenario: Include only specific tables when exporting the database + Given a WP install + + When I try `wp db export wp_cli_test.sql --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 /_mysql_data_types_cache/ + And the contents of the wp_cli_test.sql file should match /CREATE TABLE ["`]?wp_users["`]?/ + And the contents of the wp_cli_test.sql file should not match /CREATE TABLE ["`]?wp_posts["`]?/ + And the contents of the wp_cli_test.sql file should not match /CREATE TABLE ["`]?wp_options["`]?/ + + @require-sqlite + Scenario: Export database to STDOUT + Given a WP install + + When I run `wp db export -` + Then STDOUT should contain: """ - wp_options + PRAGMA foreign_keys=OFF """ + @skip-sqlite Scenario: Export database to STDOUT Given a WP install @@ -42,7 +79,7 @@ Feature: Export a WordPress database """ -- Dump completed on """ - + @skip-sqlite Scenario: Export database with mysql defaults to STDOUT Given a WP install @@ -52,6 +89,7 @@ Feature: Export a WordPress database -- Dump completed on """ + @skip-sqlite Scenario: Export database with mysql --no-defaults to STDOUT Given a WP install @@ -61,6 +99,7 @@ Feature: Export a WordPress database -- Dump completed on """ + @skip-sqlite Scenario: Export database with passed-in options Given a WP install @@ -78,6 +117,25 @@ 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 + """ + + @skip-sqlite 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..8438d06b 100644 --- a/features/db-import.feature +++ b/features/db-import.feature @@ -36,6 +36,7 @@ Feature: Import a WordPress database Success: Imported from 'wp_cli_test.sql'. """ + @require-mysql-or-mariadb Scenario: Import from STDIN Given a WP install @@ -45,6 +46,17 @@ Feature: Import a WordPress database Success: Imported from 'STDIN'. """ + # TODO: Debug the difference here. + @require-sqlite + Scenario: Import from STDIN + Given a WP install + + When I run `echo "" | wp db import -` + Then STDOUT should be: + """ + Success: Imported from 'STDIN'. + """ + Scenario: Import from database name path by default and skip speed optimization Given a WP install @@ -57,6 +69,8 @@ Feature: Import a WordPress database Success: Imported from 'wp_cli_test.sql'. """ + # SQLite doesn't support the --dbuser flag. + @require-mysql-or-mariadb Scenario: Import from database name path by default with passed-in dbuser/dbpass Given a WP install @@ -77,6 +91,8 @@ Feature: Import a WordPress database """ And STDOUT should be empty + # SQLite doesn't support the --force flag. + @require-mysql-or-mariadb Scenario: Import database with passed-in options Given a WP install And a debug.sql file: @@ -90,6 +106,9 @@ Feature: Import a WordPress database Success: Imported from 'debug.sql'. """ + # For SQLite this would fail at the `wp db create` step + # because of the missing plugin/drop-in. + @require-mysql-or-mariadb Scenario: Help runs properly at various points of a functional WP install Given an empty directory @@ -127,6 +146,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 @@ -142,7 +163,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-mysql-or-mariadb Scenario: Import db that has emoji in post Given a WP install @@ -178,3 +199,32 @@ Feature: Import a WordPress database """ 🍣 """ + + @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-optimize.feature b/features/db-optimize.feature new file mode 100644 index 00000000..4fec46d3 --- /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 try `wp db optimize` + Then STDERR should contain: + """ + Warning: Database optimization is not supported for SQLite databases + """ diff --git a/features/db-query.feature b/features/db-query.feature index 5831a536..48a50af5 100644 --- a/features/db-query.feature +++ b/features/db-query.feature @@ -21,6 +21,8 @@ Feature: Query the database with WordPress' MySQL config 1 """ + # SQLite doesn't support the --html option nor different dbuser. + @require-mysql-or-mariadb Scenario: Database querying with passed-in options Given a WP install @@ -38,6 +40,8 @@ Feature: Query the database with WordPress' MySQL config """ And STDOUT should be empty + # SQLite doesn't support the --html option nor different dbuser. + @require-mysql-or-mariadb Scenario: Database querying with MySQL defaults and passed-in options Given a WP install @@ -55,6 +59,8 @@ Feature: Query the database with WordPress' MySQL config """ And STDOUT should be empty + # SQLite doesn't support the --html option nor different dbuser. + @require-mysql-or-mariadb Scenario: Database querying with --nodefaults and passed-in options Given a WP install @@ -72,6 +78,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/features/db-repair.feature b/features/db-repair.feature new file mode 100644 index 00000000..46104b21 --- /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 try `wp db repair` + Then STDERR should contain: + """ + Warning: Database repair is not supported for SQLite databases + """ diff --git a/features/db-search.feature b/features/db-search.feature index e37826dd..9856854a 100644 --- a/features/db-search.feature +++ b/features/db-search.feature @@ -264,7 +264,6 @@ Feature: Search through the database And the return code should be 1 When I run `wp db query "CREATE TABLE no_key ( awesome_stuff TEXT );"` - And I run `wp db query "CREATE TABLE no_text ( id int(11) unsigned NOT NULL AUTO_INCREMENT, PRIMARY KEY (id) );"` And I try `wp db search example.com no_key --all-tables` Then STDOUT should be empty And STDERR should be: @@ -273,6 +272,10 @@ Feature: Search through the database """ And the return code should be 0 + @require-mysql-or-mariadb + Scenario: Search on a single site install - No text columns for table + Given a WP install + And I run `wp db query "CREATE TABLE no_text ( id int(11) unsigned NOT NULL AUTO_INCREMENT, PRIMARY KEY (id) );"` When I try `wp db search example.com no_text --all-tables` Then STDOUT should be empty And STDERR should be: @@ -869,7 +872,6 @@ Feature: Search through the database When I run `wp db search 'https://' --regex` Then the return code should be 0 - @require-wp-4.7 Scenario: Search with output options Given a WP install @@ -922,10 +924,62 @@ Feature: Search through the database """ Success: Found """ - And STDOUT should contain: + And STDOUT should match /\d tables? skipped:.*wp_term_relationships/ + And STDERR should be empty + + Scenario: Search with output options + Given a WP install + + When I run `wp db query "SELECT option_id FROM wp_options WHERE option_name = 'siteurl';" --skip-column-names | cat` + Then save STDOUT as {SITEURL_ID} + + When I run `wp db query "SELECT option_id FROM wp_options WHERE option_name = 'home';" --skip-column-names | cat` + Then save STDOUT as {HOMEURL_ID} + + When I run `wp db search example.com` + Then STDOUT should contain: + """ + wp_options:option_value + {SITEURL_ID}:https://example.com + wp_options:option_value + {HOMEURL_ID}:https://example.com + """ + + When I run `wp db search example.com --table_column_once` + Then STDOUT should contain: + """ + wp_options:option_value + {SITEURL_ID}:https://example.com + {HOMEURL_ID}:https://example.com + """ + + When I run `wp db search example.com --one_line` + Then STDOUT should contain: + """ + wp_options:option_value:{SITEURL_ID}:https://example.com + wp_options:option_value:{HOMEURL_ID}:https://example.com + """ + + When I run `wp db search example.com --table_column_once --one_line` + Then STDOUT should contain: + """ + wp_options:option_value:{SITEURL_ID}:https://example.com + wp_options:option_value:{HOMEURL_ID}:https://example.com + """ + + When I run `wp db search example.com --all-tables --before_context=0 --after_context=0 --matches_only` + Then STDOUT should not contain: + """ + : """ - 1 table skipped: wp_term_relationships. + And STDERR should be empty + + When I run `wp db search example.com --all-tables --before_context=0 --after_context=0 --stats` + Then STDOUT should contain: + """ + Success: Found """ + And STDOUT should match /\d tables? skipped:.*wp_term_relationships/ And STDERR should be empty Scenario: Search with custom colors @@ -991,6 +1045,9 @@ Feature: Search through the database """ And the return code should be 0 + # SQLite doesn't support SOURCE statement. + # TODO: Add similar test for SQLite. + @require-mysql-or-mariadb Scenario: Search should cater for field/table names that use reserved words or unusual characters Given a WP install And a esc_sql_ident.sql file: diff --git a/features/db-size.feature b/features/db-size.feature index 0958bbf1..2728fc17 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 @@ -155,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 @@ -175,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/features/db-tables.feature b/features/db-tables.feature index af8cc6ac..cb3f6810 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,40 @@ 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: + """ + wp_users + 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 +153,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: + """ + wp_users + 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 + """ + + # AUTO_INCREMENT doesn't work with SQLite. + @require-mysql-or-mariadb Scenario: Listing a site's tables should only list that site's tables Given a WP multisite install @@ -211,11 +325,14 @@ Feature: List database tables """ When I run `wp db tables '*_posts' --network` - Then STDOUT should be: + Then STDOUT should contain: """ - as_wp_2_posts as_wp_posts """ + And STDOUT should contain: + """ + as_wp_2_posts + """ When I run `wp db tables '*_posts' --scope=blog` Then STDOUT should be: @@ -224,11 +341,14 @@ Feature: List database tables """ When I run `wp db tables '*_posts' --scope=blog --network` - Then STDOUT should be: + Then STDOUT should contain: """ - as_wp_2_posts as_wp_posts """ + And STDOUT should contain: + """ + as_wp_2_posts + """ When I try `wp db tables '*_posts' --scope=global` Then STDERR should not be empty diff --git a/features/db.feature b/features/db.feature index 26b3d5f8..493b99c1 100644 --- a/features/db.feature +++ b/features/db.feature @@ -1,5 +1,6 @@ Feature: Perform database operations + @skip-sqlite 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. """ + @skip-sqlite 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 + @skip-sqlite 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 + @skip-sqlite 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 + @skip-sqlite 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 + @skip-sqlite Scenario: db repair with --quiet flag should only show errors Given a WP install @@ -194,6 +200,7 @@ Feature: Perform database operations error """ + @skip-sqlite Scenario: db repair can explicitly pass --silent to mysqlcheck Given a WP install @@ -226,6 +233,9 @@ Feature: Perform database operations total """ + Scenario: DB Query with --skip-column-names + Given a WP install + When I run `wp db query 'SELECT * FROM wp_options WHERE option_name="home"' --skip-column-names` Then STDOUT should not contain: """ @@ -236,6 +246,7 @@ Feature: Perform database operations home """ + @skip-sqlite Scenario: DB export/import Given a WP install @@ -284,11 +295,12 @@ Feature: Perform database operations 1 """ + @skip-sqlite Scenario: DB export no charset Given an empty directory And WP files - When I run `wp core config {CORE_CONFIG_SETTINGS} --dbcharset=""` + When I run `wp config create {CORE_CONFIG_SETTINGS} --dbcharset="" --skip-check` Then STDOUT should not be empty When I run `cat wp-config.php` @@ -306,11 +318,12 @@ Feature: Perform database operations Success: Exported """ + @skip-sqlite Scenario: Persist DB charset and collation Given an empty directory And WP files - When I run `wp core config {CORE_CONFIG_SETTINGS} --dbcharset=latin1 --dbcollate=latin1_spanish_ci` + When I run `wp config create {CORE_CONFIG_SETTINGS} --dbcharset=latin1 --dbcollate=latin1_spanish_ci --skip-check` Then STDOUT should not be empty When I run `wp db create` @@ -349,6 +362,7 @@ Feature: Perform database operations latin1_spanish_ci """ + @skip-sqlite 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"` @@ -368,3 +382,78 @@ 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 + And a session_yes file: + """ + y + """ + + 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 + """ diff --git a/phpcs.xml.dist b/phpcs.xml.dist index c39cec40..0beef7e6 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -55,6 +55,9 @@ */src/DB_Command\.php$ + + */src/DB_Command_SQLite\.php$ + /tests/phpstan/scan-files diff --git a/src/DB_Command.php b/src/DB_Command.php index 245a10cd..e0d7ad47 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -4,6 +4,8 @@ use WP_CLI\Utils; use cli\table\Column; +require_once __DIR__ . '/DB_Command_SQLite.php'; + /** * Performs basic database operations using credentials stored in wp-config.php. * @@ -28,6 +30,8 @@ */ class DB_Command extends WP_CLI_Command { + use DB_Command_SQLite; + /** * Legacy UTF-8 encoding for MySQL. * @@ -83,6 +87,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(); + return; + } $this->run_query( self::get_create_query(), $assoc_args ); @@ -116,6 +126,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(); + 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 ); @@ -150,6 +169,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(); + 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 ); @@ -250,6 +278,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', @@ -305,6 +339,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(), @@ -359,6 +400,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(), @@ -415,6 +463,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', @@ -518,6 +572,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', @@ -648,6 +720,8 @@ 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 { @@ -656,6 +730,12 @@ public function export( $args, $assoc_args ) { $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' ); @@ -817,12 +897,19 @@ 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 ); } + if ( $this->is_sqlite() ) { + $this->sqlite_import( $result_file, $assoc_args ); + return; + } + // Process options to MySQL. $mysql_args = array_merge( [ 'database' => DB_NAME ], @@ -1099,19 +1186,30 @@ 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 ) { + $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( + '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[] = [ @@ -1123,16 +1221,23 @@ 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_path = $this->get_sqlite_db_path(); + $db_name = $db_path ? basename( $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 ), ]; @@ -1525,6 +1630,7 @@ public function search( $args, $assoc_args ) { $tables = Utils\wp_get_table_names( $args, $assoc_args ); $search_results = []; + $is_sqlite = $this->is_sqlite(); $start_search_time = microtime( true ); @@ -1536,8 +1642,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 ) && ! $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; @@ -1759,7 +1865,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 new file mode 100644 index 00000000..342d84ea --- /dev/null +++ b/src/DB_Command_SQLite.php @@ -0,0 +1,545 @@ +run(); + $available = 0 === $result->return_code; + } + + return $available; + } + + /** + * Check if SQLite is being used. + * + * @return bool True if SQLite is detected, false otherwise. + */ + protected function is_sqlite() { + // Check if DB_ENGINE constant is defined and set to 'sqlite'. + if ( defined( 'DB_ENGINE' ) && 'sqlite' === DB_ENGINE ) { + return true; + } + + // Check if the SQLite drop-in is loaded by looking for SQLITE_DB_DROPIN_VERSION constant. + if ( defined( 'SQLITE_DB_DROPIN_VERSION' ) ) { + return true; + } + + // Check if db.php drop-in exists and contains SQLite markers. + $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 ) ) { + $db_dropin_contents = file_get_contents( $db_dropin_path ); + if ( false !== $db_dropin_contents && false !== strpos( $db_dropin_contents, 'SQLITE_DB_DROPIN_VERSION' ) ) { + return true; + } + } + + return false; + } + + /** + * Get the path to the SQLite database file. + * + * @return string|false Path to SQLite database file, or false if not found. + */ + protected function get_sqlite_db_path() { + // Check for FQDB constant (fully qualified database path). + if ( defined( 'FQDB' ) ) { + return FQDB; + } + + $wp_content_dir = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : ABSPATH . 'wp-content'; + + // Check for FQDBDIR and construct path. + $db_dir = defined( 'FQDBDIR' ) ? FQDBDIR : $wp_content_dir . '/database'; + $db_file = defined( 'DB_FILE' ) ? DB_FILE : '.ht.sqlite'; + + $db_path = rtrim( $db_dir, '/' ) . '/' . ltrim( $db_file, '/' ); + + // If the file exists, return it. + if ( file_exists( $db_path ) ) { + return $db_path; + } + + // Try alternative common locations. + $alt_paths = [ + $wp_content_dir . '/database/.ht.sqlite', + $wp_content_dir . '/.ht.sqlite', + ABSPATH . '.ht.sqlite', + ]; + + foreach ( $alt_paths as $alt_path ) { + if ( file_exists( $alt_path ) ) { + return $alt_path; + } + } + + // Return the default expected path even if it doesn't exist yet. + return $db_path; + } + + /** + * Create SQLite database. + */ + protected function sqlite_create() { + $db_path = $this->get_sqlite_db_path(); + if ( ! $db_path ) { + WP_CLI::error( 'Could not determine the database path.' ); + } + $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}" ); + } + } + + if ( file_exists( $db_path ) ) { + WP_CLI::error( 'Database already exists.' ); + } + + if ( ! $this->is_sqlite3_available() ) { + WP_CLI::error( 'The sqlite3 CLI binary is required but not found. Please install SQLite3.' ); + } + + $command = Utils\esc_cmd( 'sqlite3 %s %s', $db_path, '' ); + + WP_CLI::debug( "Running shell command: {$command}", 'db' ); + + $result = \WP_CLI\Process::create( $command, null, null )->run(); + + if ( 0 !== $result->return_code ) { + WP_CLI::error( 'Could not create database' ); + } + + WP_CLI::success( 'Database created.' ); + } + + /** + * Drop SQLite database. + */ + 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.' ); + } + + if ( ! unlink( $db_path ) ) { + WP_CLI::error( "Could not delete database file: {$db_path}" ); + } + + WP_CLI::success( 'Database dropped.' ); + } + + /** + * Reset SQLite database. + */ + 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 ) ) { + WP_CLI::error( "Could not delete database file: {$db_path}" ); + } + } + + // 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}" ); + } + } + + if ( ! $this->is_sqlite3_available() ) { + WP_CLI::error( 'The sqlite3 CLI binary is required but not found. Please install SQLite3.' ); + } + + $command = Utils\esc_cmd( 'sqlite3 %s %s', $db_path, '' ); + + WP_CLI::debug( "Running shell command: {$command}", 'db' ); + + $result = \WP_CLI\Process::create( $command, null, null )->run(); + + if ( 0 !== $result->return_code ) { + WP_CLI::error( 'Could not create database' ); + } + + 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 = [] ) { + global $wpdb; + + if ( ! isset( $wpdb ) || ! $wpdb instanceof \WP_SQLite_DB ) { + WP_CLI::error( 'SQLite database not available.' ); + } + + $skip_column_names = Utils\get_flag_value( $assoc_args, 'skip-column-names', false ); + + 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 results using the Formatter class. + $headers = array_keys( $results[0] ); + $this->display_query_results( $headers, $results, $skip_column_names ); + } + } catch ( Exception $e ) { + WP_CLI::error( 'Query failed: ' . $e->getMessage() ); + } + } + + /** + * Display query results using the Formatter class. + * + * @param array $headers Column headers. + * @param array $rows Data rows. + * @param bool $skip_column_names Whether to skip displaying column names. + */ + protected function display_query_results( $headers, $rows, $skip_column_names = false ) { + if ( $skip_column_names ) { + // Display rows without headers - just the values. + foreach ( $rows as $row ) { + WP_CLI::line( implode( "\t", array_values( $row ) ) ); + } + } else { + // Use the Formatter class to display results as a table. + $assoc_args = []; + $formatter = new Formatter( $assoc_args, $headers ); + $formatter->display_items( $rows ); + } + } + + /** + * 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 ( ! $db_path ) { + WP_CLI::error( 'Could not determine the database path.' ); + } + + if ( ! file_exists( $db_path ) ) { + WP_CLI::error( 'Database does not exist.' ); + } + + if ( ! $this->is_sqlite3_available() ) { + WP_CLI::error( 'The sqlite3 binary could not be found. Please install sqlite3 to use the export command.' ); + } + $temp_db = tempnam( sys_get_temp_dir(), 'temp.db' ); + if ( false === $temp_db ) { + WP_CLI::error( 'Could not create temporary database file for export.' ); + } + + if ( ! copy( $db_path, $temp_db ) ) { + // Clean up temporary file if the copy operation fails. + unlink( $temp_db ); + WP_CLI::error( 'Could not copy database to temporary file for export.' ); + } + + $exclude_tables = []; + + // When passing --tables, exclude everything *except* the tables requested. + if ( isset( $assoc_args['tables'] ) ) { + $include_tables = explode( ',', trim( $assoc_args['tables'], ',' ) ); + unset( $assoc_args['tables'] ); + + // Use the sqlite3 binary to fetch all table names + $query = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';"; + + // Build the command safely + $command = 'sqlite3 ' . escapeshellarg( $temp_db ) . ' ' . escapeshellarg( $query ); + + WP_CLI::debug( "Running shell command: {$command}", 'db' ); + + $result = \WP_CLI\Process::create( $command, null, null )->run(); + + if ( 0 !== $result->return_code ) { + WP_CLI::error( 'Could not export database' ); + } + + $all_tables = explode( "\n", $result->stdout ); + + $exclude_tables = array_diff( $all_tables, $include_tables ); + } + + if ( isset( $assoc_args['exclude_tables'] ) ) { + $exclude_tables = explode( ',', trim( $assoc_args['exclude_tables'], ',' ) ); + unset( $assoc_args['exclude_tables'] ); + } + + // Always exclude this one created by the drop-in. + $exclude_tables[] = '_mysql_data_types_cache'; + + $exclude_tables = array_unique( array_filter( $exclude_tables ) ); + + // Build DROP TABLE statements with safely-escaped identifiers. + $drop_statements = array(); + foreach ( $exclude_tables as $table ) { + $escaped_identifier = '"' . str_replace( '"', '""', $table ) . '"'; + $drop_statements[] = sprintf( 'DROP TABLE %s;', $escaped_identifier ); + } + + if ( ! empty( $drop_statements ) ) { + $args = array_merge( array( 'sqlite3', $temp_db ), $drop_statements ); + $placeholders = array_fill( 0, count( $args ), '%s' ); + $command = Utils\esc_cmd( implode( ' ', $placeholders ), ...$args ); + + WP_CLI::debug( "Running shell command: {$command}", 'db' ); + + $result = \WP_CLI\Process::create( $command, null, null )->run(); + + if ( 0 !== $result->return_code ) { + WP_CLI::error( 'Could not export database' ); + } + } + + $command = Utils\esc_cmd( 'sqlite3 %s .dump', $temp_db ); + + WP_CLI::debug( "Running shell command: {$command}", 'db' ); + + $result = \WP_CLI\Process::create( $command, null, null )->run(); + + if ( 0 !== $result->return_code ) { + WP_CLI::error( 'Could not export database' ); + } + + $stdout = ( '-' === $file ); + + if ( $stdout ) { + WP_CLI::line( $result->stdout ); + } elseif ( false === file_put_contents( $file, $result->stdout ) ) { + // Clean up temporary file and surface a clear error if the export cannot be written. + if ( file_exists( $temp_db ) ) { + unlink( $temp_db ); + } + WP_CLI::error( "Could not write exported database to '{$file}'. Please check that the path is writable." ); + } + + unlink( $temp_db ); + + 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 ) { + $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.' ); + } + + if ( ! $this->is_sqlite3_available() ) { + WP_CLI::error( 'The sqlite3 CLI binary could not be found. Please ensure it is installed and available on your PATH.' ); + } + if ( '-' === $file ) { + $contents = file_get_contents( 'php://stdin' ); + if ( false === $contents ) { + WP_CLI::error( 'Failed to read from stdin.' ); + } + + $file = 'STDIN'; + } elseif ( ! is_readable( $file ) ) { + WP_CLI::error( sprintf( 'Import file missing or not readable: %s', $file ) ); + } else { + $contents = (string) file_get_contents( $file ); + } + + // Ignore errors about unique constraints and existing indexes. + $contents = str_replace( 'INSERT INTO', 'INSERT OR IGNORE INTO', $contents ); + $contents = preg_replace( '/\bCREATE TABLE (?!IF NOT EXISTS\b)/i', 'CREATE TABLE IF NOT EXISTS ', $contents ); + $contents = preg_replace( '/\bCREATE TRIGGER (?!IF NOT EXISTS\b)/i', 'CREATE TRIGGER IF NOT EXISTS ', (string) $contents ); + $contents = preg_replace( '/\bCREATE VIEW (?!IF NOT EXISTS\b)/i', 'CREATE VIEW IF NOT EXISTS ', (string) $contents ); + $contents = preg_replace( '/\bCREATE INDEX (?!IF NOT EXISTS\b)/i', 'CREATE INDEX IF NOT EXISTS ', (string) $contents ); + $contents = preg_replace( '/\bCREATE UNIQUE INDEX (?!IF NOT EXISTS\b)/i', 'CREATE UNIQUE INDEX IF NOT EXISTS ', (string) $contents ); + + $import_file = tempnam( sys_get_temp_dir(), 'temp.db' ); + file_put_contents( $import_file, $contents ); + + // Build sqlite3 command. + $command_parts = [ 'sqlite3' ]; + + if ( ! Utils\get_flag_value( $assoc_args, 'skip-optimization' ) ) { + $command_parts[] = '-cmd'; + $command_parts[] = 'PRAGMA foreign_keys=OFF;'; + $command_parts[] = '-cmd'; + $command_parts[] = 'PRAGMA ignore_check_constraints=ON;'; + $command_parts[] = '-cmd'; + $command_parts[] = 'PRAGMA synchronous=OFF;'; + $command_parts[] = '-cmd'; + $command_parts[] = 'PRAGMA journal_mode=MEMORY;'; + } + + $command_parts[] = $db_path; + + // Build a properly escaped string command. Process::create() requires a string, not an array. + $command = Utils\esc_cmd( + implode( ' ', array_fill( 0, count( $command_parts ), '%s' ) ), + ...$command_parts + ); + + // Pass the .read dot-command as a single quoted argument (sqlite3 reads it as SQL input). + $command .= ' ' . escapeshellarg( '.read ' . $import_file ); + + WP_CLI::debug( "Running shell command: {$command}", 'db' ); + + $result = \WP_CLI\Process::create( $command, null, null )->run(); + unlink( $import_file ); + + if ( 0 !== $result->return_code ) { + WP_CLI::error( 'Could not import database.' ); + } + + WP_CLI::success( sprintf( "Imported from '%s'.", $file ) ); + } + + /** + * 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 ( ! $db_path || ! file_exists( $db_path ) ) { + return 0; + } + + $size = filesize( $db_path ); + if ( false === $size ) { + return 0; + } + + return $size; + } + + /** + * 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; + } + + // 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' ) ) { + $required_files = [ + ABSPATH . WPINC . '/compat.php', + ABSPATH . WPINC . '/plugin.php', + // Defines `wp_debug_backtrace_summary()` as used by wpdb. + ABSPATH . WPINC . '/functions.php', + ABSPATH . WPINC . '/class-wpdb.php', + ]; + + foreach ( $required_files as $required_file ) { + if ( file_exists( $required_file ) ) { + require_once $required_file; + } + } + } + + // Load the db.php drop-in. + require_once $db_dropin_path; + } +} 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 52d4aff041cea30233167fd623eeb3fcc39cda23 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 24 Mar 2026 13:19:22 +0100 Subject: [PATCH 34/53] Harden test involving sqlite db name (#316) * Harden test involving sqlite db name The filename was changed in the latest sqlite plugin version. Accommodate for both * Update features/db-size.feature Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update features/db-size.feature Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- features/db-size.feature | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/features/db-size.feature b/features/db-size.feature index 2728fc17..b21d9949 100644 --- a/features/db-size.feature +++ b/features/db-size.feature @@ -213,19 +213,12 @@ Feature: Display database size Given a WP install When I run `wp db size --size_format=b --format=csv` - Then STDOUT should contain: - """ - Name,Size - .ht.sqlite," - """ + Then STDOUT should match /Name,Size\n\.ht\.sqlite(\.php)?,"/ 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":" - """ + Then STDOUT should match /^\[\{"Name":"\.ht\.sqlite(\.php)?","Size":"/ But STDOUT should not be a number From 39be7312078432ce5005ff2d285493b7f44c875d Mon Sep 17 00:00:00 2001 From: swissspidy Date: Thu, 26 Mar 2026 19:52:39 +0000 Subject: [PATCH 35/53] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 32404820..4aadc6bc 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -36,9 +36,6 @@ jobs: - name: Install Composer dependencies & cache dependencies if: steps.check_composer_file.outputs.files_exists == 'true' - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v3 + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4 env: COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} - with: - # Bust the cache at least once a month - output format: YYYY-MM. - custom-cache-suffix: $(date -u "+%Y-%m") From bc17608003dfa44d73e51a7971b023e7f64653a9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:02:11 +0100 Subject: [PATCH 36/53] Regenerate README file (#317) Co-authored-by: schlessera <83631+schlessera@users.noreply.github.com> --- README.md | 70 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 95b2df1d..5f253473 100644 --- a/README.md +++ b/README.md @@ -648,39 +648,39 @@ Defaults to searching through all tables registered to $wpdb. On multisite, this - count --- -The percent color codes available are: - -| Code | Color -| ---- | ----- -| %y | Yellow (dark) (mustard) -| %g | Green (dark) -| %b | Blue (dark) -| %r | Red (dark) -| %m | Magenta (dark) -| %c | Cyan (dark) -| %w | White (dark) (light gray) -| %k | Black -| %Y | Yellow (bright) -| %G | Green (bright) -| %B | Blue (bright) -| %R | Red (bright) -| %M | Magenta (bright) -| %C | Cyan (bright) -| %W | White -| %K | Black (bright) (dark gray) -| %3 | Yellow background (dark) (mustard) -| %2 | Green background (dark) -| %4 | Blue background (dark) -| %1 | Red background (dark) -| %5 | Magenta background (dark) -| %6 | Cyan background (dark) -| %7 | White background (dark) (light gray) -| %0 | Black background -| %8 | Reverse -| %U | Underline -| %F | Blink (unlikely to work) - -They can be concatenated. For instance, the default match color of black on a mustard (dark yellow) background `%3%k` can be made black on a bright yellow background with `%Y%0%8`. + The percent color codes available are: + + | Code | Color + | ---- | ----- + | %y | Yellow (dark) (mustard) + | %g | Green (dark) + | %b | Blue (dark) + | %r | Red (dark) + | %m | Magenta (dark) + | %c | Cyan (dark) + | %w | White (dark) (light gray) + | %k | Black + | %Y | Yellow (bright) + | %G | Green (bright) + | %B | Blue (bright) + | %R | Red (bright) + | %M | Magenta (bright) + | %C | Cyan (bright) + | %W | White + | %K | Black (bright) (dark gray) + | %3 | Yellow background (dark) (mustard) + | %2 | Green background (dark) + | %4 | Blue background (dark) + | %1 | Red background (dark) + | %5 | Magenta background (dark) + | %6 | Cyan background (dark) + | %7 | White background (dark) (light gray) + | %0 | Black background + | %8 | Reverse + | %U | Underline + | %F | Blink (unlikely to work) + + They can be concatenated. For instance, the default match color of black on a mustard (dark yellow) background `%3%k` can be made black on a bright yellow background with `%Y%0%8`. **AVAILABLE FIELDS** @@ -1020,6 +1020,10 @@ Want to contribute a new feature? Please first [open a new issue](https://github Once you've decided to commit the time to seeing your pull request through, [please follow our guidelines for creating a pull request](https://make.wordpress.org/cli/handbook/pull-requests/) to make sure it's a pleasant experience. See "[Setting up](https://make.wordpress.org/cli/handbook/pull-requests/#setting-up)" for details specific to working on this package locally. +### License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. + ## Support GitHub issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support From 5b21f21f5cc455f7b2a5978221698200fb7b51b8 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 26 Mar 2026 21:06:19 +0100 Subject: [PATCH 37/53] Update PHP_CodeSniffer repository links --- phpcs.xml.dist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 0beef7e6..b439f14f 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -5,8 +5,8 @@ From faf63a19fabfe3dc80239a028a27e74d98c18f2c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 27 Mar 2026 12:40:16 +0100 Subject: [PATCH 38/53] Add codecov config --- codecov.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..e69de29b From 33c4c58a17b21b2d708713d0e04df9a392b9c412 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:49:25 +0100 Subject: [PATCH 39/53] Regenerate README file (#318) Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f253473..642e70a5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ wp-cli/db-command Performs basic database operations using credentials stored in wp-config.php. -[![Testing](https://github.com/wp-cli/db-command/actions/workflows/testing.yml/badge.svg)](https://github.com/wp-cli/db-command/actions/workflows/testing.yml) +[![Testing](https://github.com/wp-cli/db-command/actions/workflows/testing.yml/badge.svg)](https://github.com/wp-cli/db-command/actions/workflows/testing.yml) [![Code Coverage](https://codecov.io/gh/wp-cli/db-command/branch/main/graph/badge.svg)](https://codecov.io/gh/wp-cli/db-command/tree/main) Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) | [Support](#support) From 392f5b8577a916afe16afc23975daea43fbdd307 Mon Sep 17 00:00:00 2001 From: Janw Oostendorp Date: Tue, 7 Apr 2026 10:42:57 +0200 Subject: [PATCH 40/53] Specify scope options in same format as other arguments that have a list (#321) --- src/DB_Command.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index e0d7ad47..78cee981 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -957,7 +957,22 @@ public function import( $args, $assoc_args ) { * : List tables based on wildcard search, e.g. 'wp_*_options' or 'wp_post?'. * * [--scope=] - * : Can be all, global, ms_global, blog, or old tables. Defaults to all. + * : List tables based on the scope. + * + * - all: returns 'all' and 'global' tables. No old tables are returned. + * - blog: returns the blog-level tables for the queried blog. + * - global: returns the global tables for the installation, returning multisite tables only on multisite. + * - ms_global: returns the multisite global tables, regardless if current installation is multisite. + * - old: returns tables which are deprecated. + * --- + * default: all + * options: + * - all + * - blog + * - global + * - ms_global + * - old + * --- * * [--network] * : List all the tables in a multisite install. From f035d64e1a26b6bd45aa272cad21ab77ea6d6bd2 Mon Sep 17 00:00:00 2001 From: swissspidy <841956+swissspidy@users.noreply.github.com> Date: Tue, 7 Apr 2026 08:43:53 +0000 Subject: [PATCH 41/53] Regenerate README file --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 642e70a5..8304ea9a 100644 --- a/README.md +++ b/README.md @@ -759,7 +759,22 @@ Defaults to all tables registered to the $wpdb database handler. List tables based on wildcard search, e.g. 'wp_*_options' or 'wp_post?'. [--scope=] - Can be all, global, ms_global, blog, or old tables. Defaults to all. + List tables based on the scope. + + - all: returns 'all' and 'global' tables. No old tables are returned. + - blog: returns the blog-level tables for the queried blog. + - global: returns the global tables for the installation, returning multisite tables only on multisite. + - ms_global: returns the multisite global tables, regardless if current installation is multisite. + - old: returns tables which are deprecated. + --- + default: all + options: + - all + - blog + - global + - ms_global + - old + --- [--network] List all the tables in a multisite install. From 7e19f23339e05fb6b8b5d165750082b5f5960d25 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 14 Apr 2026 17:29:42 +0200 Subject: [PATCH 42/53] Tests: improve Windows compatibility (#323) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- features/db-search.feature | 53 +++++++++++++------------- features/db-size.feature | 8 ++++ features/db.feature | 16 ++++---- src/DB_Command_SQLite.php | 77 ++++++++++++++++++++++++++++++-------- 4 files changed, 105 insertions(+), 49 deletions(-) diff --git a/features/db-search.feature b/features/db-search.feature index 9856854a..f5e3225e 100644 --- a/features/db-search.feature +++ b/features/db-search.feature @@ -679,19 +679,19 @@ Feature: Search through the database :あいうえおかきくけこさしすせとたちつてと """ - When I run `wp db search 'ppppp' --before_context=3 --after_context=4` + When I run `wp db search "ppppp" --before_context=3 --after_context=4` Then STDOUT should contain: """ :ムnöppppp """ - When I run `wp db search 'ppppp' --before_context=1 --after_context=1` + When I run `wp db search "ppppp" --before_context=1 --after_context=1` Then STDOUT should contain: """ :öppppp """ - When I run `wp db search 'ムn' --before_context=2 --after_context=1` + When I run `wp db search "ムn" --before_context=2 --after_context=1` Then STDOUT should contain: """ :llムnö @@ -701,7 +701,7 @@ Feature: Search through the database :llムnöp """ - When I run `wp db search 'ムn' --before_context=2 --after_context=2` + When I run `wp db search "ムn" --before_context=2 --after_context=2` Then STDOUT should contain: """ :llムnöp @@ -720,7 +720,7 @@ Feature: Search through the database # Note ö is o with combining umlaut. And I run `wp option update regextst_combining 'lllllムnöppppp'` - When I run `wp db search 'https?:\/\/example.c.m' --regex` + When I run `wp db search "https?:\/\/example.c.m" --regex` Then STDOUT should contain: """ wp_options:option_value @@ -731,10 +731,10 @@ Feature: Search through the database [...] """ - When I run `wp db search 'unfindable' --regex` + When I run `wp db search "unfindable" --regex` Then STDOUT should be empty - When I try `wp db search 'unfindable' --regex --regex-flags='abcd'` + When I try `wp db search "unfindable" --regex --regex-flags="abcd"` Then STDERR should contain: """ unfindable @@ -745,42 +745,42 @@ Feature: Search through the database """ And the return code should be 1 - When I try `wp db search 'unfindable' --regex --regex-delimiter='1'` + When I try `wp db search "unfindable" --regex --regex-delimiter="1"` Then STDERR should be: """ Error: The regex '1unfindable1' fails. """ And the return code should be 1 - When I try `wp db search 'regex error)' --regex` + When I try `wp db search "regex error)" --regex` Then STDERR should be: """ Error: The regex pattern 'regex error)' with default delimiter 'chr(1)' and no flags fails. """ And the return code should be 1 - When I try `wp db search 'regex error)' --regex --regex-flags=u` + When I try `wp db search "regex error)" --regex --regex-flags=u` Then STDERR should be: """ Error: The regex pattern 'regex error)' with default delimiter 'chr(1)' and flags 'u' fails. """ And the return code should be 1 - When I try `wp db search 'regex error)' --regex --regex-delimiter=/` + When I try `wp db search "regex error)" --regex --regex-delimiter=#` Then STDERR should be: """ - Error: The regex '/regex error)/' fails. + Error: The regex '#regex error)#' fails. """ And the return code should be 1 - When I try `wp db search 'regex error)' --regex --regex-delimiter=/ --regex-flags=u` + When I try `wp db search "regex error)" --regex --regex-delimiter=# --regex-flags=u` Then STDERR should be: """ - Error: The regex '/regex error)/u' fails. + Error: The regex '#regex error)#u' fails. """ And the return code should be 1 - When I run `wp db search '[0-9é]+?https:' --regex --regex-flags=u --before_context=0 --after_context=0` + When I run `wp db search "[0-9é]+?https:" --regex --regex-flags=u --before_context=0 --after_context=0` Then STDOUT should contain: """ :1234567890123456789éhttps: @@ -794,7 +794,7 @@ Feature: Search through the database [...] """ - When I run `wp db search 'htt(p(s):)\/\/' --regex --before_context=1 --after_context=3` + When I run `wp db search "htt(p(s):)\/\/" --regex --before_context=1 --after_context=3` Then STDOUT should contain: """ :あhttps://reg [...] éhttps://reg @@ -804,7 +804,7 @@ Feature: Search through the database rege """ - When I run `wp db search 'https://' --regex --regex-delimiter=# --before_context=9 --after_context=11` + When I run `wp db search "https://" --regex --regex-delimiter=# --before_context=9 --after_context=11` Then STDOUT should contain: """ :2345é789あhttps://regextst.co [...] 23456789éhttps://regextst.co @@ -814,10 +814,10 @@ Feature: Search through the database regextst.com """ - When I run `wp db search 'httPs://' --regex --regex-delimiter=# --before_context=3 --after_context=0` + When I run `wp db search "httPs://" --regex --regex-delimiter=# --before_context=3 --after_context=0` Then STDOUT should be empty - When I run `wp db search 'httPs://' --regex --regex-flags=i --regex-delimiter=# --before_context=3 --after_context=0` + When I run `wp db search "httPs://" --regex --regex-flags=i --regex-delimiter=# --before_context=3 --after_context=0` Then STDOUT should contain: """ :89あhttps:// [...] 89éhttps:// @@ -827,19 +827,19 @@ Feature: Search through the database https://r """ - When I run `wp db search 'ppppp' --regex --before_context=3 --after_context=4` + When I run `wp db search "ppppp" --regex --before_context=3 --after_context=4` Then STDOUT should contain: """ :ムnöppppp """ - When I run `wp db search 'ppppp' --regex --before_context=1 --after_context=1` + When I run `wp db search "ppppp" --regex --before_context=1 --after_context=1` Then STDOUT should contain: """ :öppppp """ - When I run `wp db search 'ムn' --before_context=2 --after_context=1` + When I run `wp db search "ムn" --before_context=2 --after_context=1` Then STDOUT should contain: """ :llムnö @@ -849,7 +849,7 @@ Feature: Search through the database :llムnöp """ - When I run `wp db search 'ムn' --regex --before_context=2 --after_context=2` + When I run `wp db search "ムn" --regex --before_context=2 --after_context=2` Then STDOUT should contain: """ :llムnöp @@ -859,7 +859,7 @@ Feature: Search through the database :llムnöpp """ - When I run `wp db search 't\.c' --regex --before_context=1 --after_context=1` + When I run `wp db search "t\.c" --regex --before_context=1 --after_context=1` Then STDOUT should contain: """ :st.co [...] st.co [...] st.co [...] 0t.co @@ -869,7 +869,7 @@ Feature: Search through the database st.com """ - When I run `wp db search 'https://' --regex` + When I run `wp db search "https://" --regex` Then the return code should be 0 Scenario: Search with output options @@ -982,6 +982,7 @@ Feature: Search through the database And STDOUT should match /\d tables? skipped:.*wp_term_relationships/ And STDERR should be empty + @skip-windows Scenario: Search with custom colors Given a WP install @@ -1060,7 +1061,7 @@ Feature: Search through the database When I run `wp db query "SOURCE esc_sql_ident.sql;"` Then STDERR should be empty - When I run `wp db search 'v_v' TABLE --all-tables` + When I run `wp db search "v_v" TABLE --all-tables` Then STDOUT should be: """ TABLE:VALUES diff --git a/features/db-size.feature b/features/db-size.feature index b21d9949..17f0d687 100644 --- a/features/db-size.feature +++ b/features/db-size.feature @@ -32,6 +32,8 @@ Feature: Display database size B """ + # On CI, SQLite on Windows is missing the dbstat extension. + @skip-windows Scenario: Display only table sizes for a WordPress install Given a WP install @@ -81,6 +83,8 @@ Feature: Display database size KB """ + # On CI, SQLite on Windows is missing the dbstat extension. + @skip-windows Scenario: Display only table sizes in a human readable format for a WordPress install Given a WP install @@ -222,6 +226,8 @@ Feature: Display database size But STDOUT should not be a number + # On CI, SQLite on Windows is missing the dbstat extension. + @skip-windows Scenario: Display all table sizes for a WordPress install Given a WP install @@ -282,6 +288,8 @@ Feature: Display database size [{"Name":"wp_posts", """ + # On CI, SQLite on Windows is missing the dbstat extension. + @skip-windows Scenario: Display ordered table sizes for a WordPress install Given a WP install diff --git a/features/db.feature b/features/db.feature index 493b99c1..7a58c3a2 100644 --- a/features/db.feature +++ b/features/db.feature @@ -256,7 +256,7 @@ Feature: Perform database operations 1 """ - When I run `wp db export /tmp/wp-cli-behat.sql` + When I run `wp db export wp-cli-behat.sql` Then STDOUT should contain: """ Success: Exported @@ -283,7 +283,7 @@ Feature: Perform database operations When I try `wp post list --format=count` Then STDERR should not be empty - When I run `wp db import /tmp/wp-cli-behat.sql` + When I run `wp db import wp-cli-behat.sql` Then STDOUT should contain: """ Success: Imported @@ -312,7 +312,7 @@ Feature: Perform database operations When I run `wp db create` Then STDOUT should not be empty - When I run `wp db export /tmp/wp-cli-behat.sql` + When I run `wp db export wp-cli-behat.sql` Then STDOUT should contain: """ Success: Exported @@ -383,7 +383,8 @@ Feature: Perform database operations Query succeeded. Rows affected: 1 """ - @require-sqlite + @require-sqlite @skip-windows + # Skipped on Windows due to persistent file locking issues when run via Behat. Scenario: SQLite DB CRUD operations Given a WP install And a session_yes file: @@ -420,7 +421,8 @@ Feature: Perform database operations total """ - @require-sqlite + @require-sqlite @skip-windows + # Skipped on Windows due to persistent file locking issues when run via Behat. Scenario: SQLite DB export/import Given a WP install And a session_yes file: @@ -434,7 +436,7 @@ Feature: Perform database operations 1 """ - When I run `wp db export /tmp/wp-cli-sqlite-behat.sql` + When I run `wp db export wp-cli-sqlite-behat.sql` Then STDOUT should contain: """ Success: Exported @@ -446,7 +448,7 @@ Feature: Perform database operations Success: Database reset """ - When I run `wp db import /tmp/wp-cli-sqlite-behat.sql` + When I run `wp db import wp-cli-sqlite-behat.sql` Then STDOUT should contain: """ Success: Imported diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php index 342d84ea..77893a00 100644 --- a/src/DB_Command_SQLite.php +++ b/src/DB_Command_SQLite.php @@ -20,7 +20,7 @@ protected function is_sqlite3_available() { static $available = null; if ( null === $available ) { - $result = \WP_CLI\Process::create( '/usr/bin/env sqlite3 --version', null, null )->run(); + $result = \WP_CLI\Process::create( Utils\force_env_on_nix_systems( 'sqlite3' ) . ' --version', null, null )->run(); $available = 0 === $result->return_code; } @@ -149,7 +149,12 @@ protected function sqlite_drop() { WP_CLI::error( 'Database does not exist.' ); } - if ( ! unlink( $db_path ) ) { + global $wpdb; + if ( $wpdb instanceof \wpdb ) { + $wpdb->close(); + } + + if ( ! @unlink( $db_path ) ) { WP_CLI::error( "Could not delete database file: {$db_path}" ); } @@ -168,7 +173,12 @@ protected function sqlite_reset() { // Delete if exists. if ( file_exists( $db_path ) ) { - if ( ! unlink( $db_path ) ) { + global $wpdb; + if ( $wpdb instanceof \wpdb ) { + $wpdb->close(); + } + + if ( ! @unlink( $db_path ) ) { WP_CLI::error( "Could not delete database file: {$db_path}" ); } } @@ -292,6 +302,7 @@ protected function sqlite_export( $file, $assoc_args ) { if ( false === $temp_db ) { WP_CLI::error( 'Could not create temporary database file for export.' ); } + $temp_db = str_replace( '\\', '/', $temp_db ); if ( ! copy( $db_path, $temp_db ) ) { // Clean up temporary file if the copy operation fails. @@ -320,7 +331,8 @@ protected function sqlite_export( $file, $assoc_args ) { WP_CLI::error( 'Could not export database' ); } - $all_tables = explode( "\n", $result->stdout ); + $all_tables = array_map( 'trim', explode( "\n", $result->stdout ) ); + $all_tables = array_filter( $all_tables ); $exclude_tables = array_diff( $all_tables, $include_tables ); } @@ -342,27 +354,46 @@ protected function sqlite_export( $file, $assoc_args ) { $drop_statements[] = sprintf( 'DROP TABLE %s;', $escaped_identifier ); } - if ( ! empty( $drop_statements ) ) { - $args = array_merge( array( 'sqlite3', $temp_db ), $drop_statements ); - $placeholders = array_fill( 0, count( $args ), '%s' ); - $command = Utils\esc_cmd( implode( ' ', $placeholders ), ...$args ); + $init_file = tempnam( sys_get_temp_dir(), 'export_init' ); + if ( false === $init_file ) { + if ( file_exists( $temp_db ) ) { + unlink( $temp_db ); + } - WP_CLI::debug( "Running shell command: {$command}", 'db' ); + WP_CLI::error( 'Failed to create temporary SQLite init file for export.' ); + } + $init_file = str_replace( '\\', '/', $init_file ); - $result = \WP_CLI\Process::create( $command, null, null )->run(); + $init_contents = ''; + if ( ! empty( $drop_statements ) ) { + $init_contents .= implode( "\n", $drop_statements ) . "\n"; + } + $init_contents .= ".dump\n"; - if ( 0 !== $result->return_code ) { - WP_CLI::error( 'Could not export database' ); + $bytes_written = file_put_contents( $init_file, $init_contents ); + if ( false === $bytes_written ) { + if ( file_exists( $init_file ) ) { + unlink( $init_file ); } + if ( file_exists( $temp_db ) ) { + unlink( $temp_db ); + } + WP_CLI::error( 'Could not export database' ); } - $command = Utils\esc_cmd( 'sqlite3 %s .dump', $temp_db ); + $command = Utils\esc_cmd( 'sqlite3 -init %s %s .exit', $init_file, $temp_db ); WP_CLI::debug( "Running shell command: {$command}", 'db' ); $result = \WP_CLI\Process::create( $command, null, null )->run(); + if ( file_exists( $init_file ) ) { + unlink( $init_file ); + } if ( 0 !== $result->return_code ) { + if ( file_exists( $temp_db ) ) { + unlink( $temp_db ); + } WP_CLI::error( 'Could not export database' ); } @@ -401,6 +432,7 @@ protected function sqlite_import( $file, $assoc_args ) { if ( ! $db_path ) { WP_CLI::error( 'Could not determine the database path.' ); } + $db_path = str_replace( '\\', '/', $db_path ); if ( ! file_exists( $db_path ) ) { WP_CLI::error( 'Database does not exist.' ); @@ -431,7 +463,18 @@ protected function sqlite_import( $file, $assoc_args ) { $contents = preg_replace( '/\bCREATE UNIQUE INDEX (?!IF NOT EXISTS\b)/i', 'CREATE UNIQUE INDEX IF NOT EXISTS ', (string) $contents ); $import_file = tempnam( sys_get_temp_dir(), 'temp.db' ); - file_put_contents( $import_file, $contents ); + if ( false === $import_file ) { + WP_CLI::error( 'Failed to create a temporary file for SQLite import.' ); + } + + $import_file = str_replace( '\\', '/', $import_file ); + $bytes_written = file_put_contents( $import_file, $contents ); + if ( false === $bytes_written ) { + if ( file_exists( $import_file ) ) { + unlink( $import_file ); + } + WP_CLI::error( sprintf( 'Failed to write SQLite import data to temporary file: %s', $import_file ) ); + } // Build sqlite3 command. $command_parts = [ 'sqlite3' ]; @@ -447,6 +490,8 @@ protected function sqlite_import( $file, $assoc_args ) { $command_parts[] = 'PRAGMA journal_mode=MEMORY;'; } + $command_parts[] = '-init'; + $command_parts[] = $import_file; $command_parts[] = $db_path; // Build a properly escaped string command. Process::create() requires a string, not an array. @@ -455,8 +500,8 @@ protected function sqlite_import( $file, $assoc_args ) { ...$command_parts ); - // Pass the .read dot-command as a single quoted argument (sqlite3 reads it as SQL input). - $command .= ' ' . escapeshellarg( '.read ' . $import_file ); + // Pass .exit to quit interactive mode after running -init. + $command .= ' ' . escapeshellarg( '.exit' ); WP_CLI::debug( "Running shell command: {$command}", 'db' ); From 2f0523f58a8928736766a7c206f02f0ce174ebdd Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:44:55 +0200 Subject: [PATCH 43/53] Docs: Remove `--add-drop-table` from `wp db export` OPTIONS; update example (#324) Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- src/DB_Command.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 78cee981..03b578af 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -664,16 +664,13 @@ public function query( $args, $assoc_args ) { * [--porcelain] * : Output filename for the exported database. * - * [--add-drop-table] - * : Include a `DROP TABLE IF EXISTS` statement before each `CREATE TABLE` statement. - * * [--defaults] * : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration. * * ## EXAMPLES * - * # Export database with drop query included - * $ wp db export --add-drop-table + * # Export database with `--skip-opt` and `--add-drop-table` mysqldump flags + * $ wp db export --skip-opt --add-drop-table * Success: Exported to 'wordpress_dbase-db72bb5.sql'. * * # Export certain tables From 36f3ce492209e77ae51b70434eb0ec323f59eaa8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:46:25 +0200 Subject: [PATCH 44/53] Regenerate README file (#325) Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8304ea9a..2aae83a2 100644 --- a/README.md +++ b/README.md @@ -431,7 +431,7 @@ To confirm the ID for the site you want to query, you can use the `wp site list` Exports the database to a file or to STDOUT. ~~~ -wp db export [] [--dbuser=] [--dbpass=] [--=] [--tables=] [--exclude_tables=] [--include-tablespaces] [--porcelain] [--add-drop-table] [--defaults] +wp db export [] [--dbuser=] [--dbpass=] [--=] [--tables=] [--exclude_tables=] [--include-tablespaces] [--porcelain] [--defaults] ~~~ **Alias:** `dump` @@ -466,16 +466,13 @@ Runs `mysqldump` utility using `DB_HOST`, `DB_NAME`, `DB_USER` and [--porcelain] Output filename for the exported database. - [--add-drop-table] - Include a `DROP TABLE IF EXISTS` statement before each `CREATE TABLE` statement. - [--defaults] Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration. **EXAMPLES** - # Export database with drop query included - $ wp db export --add-drop-table + # Export database with `--skip-opt` and `--add-drop-table` mysqldump flags + $ wp db export --skip-opt --add-drop-table Success: Exported to 'wordpress_dbase-db72bb5.sql'. # Export certain tables From 8a1bd2c8fbda0eb4a819b198a81d05852f81875d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristi=20Burc=C4=83?= Date: Tue, 21 Apr 2026 16:56:30 +0100 Subject: [PATCH 45/53] db export: Avoid exec() in option probe (#326) --- features/db-export.feature | 13 +++++++++++++ src/DB_Command.php | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/features/db-export.feature b/features/db-export.feature index 7635ffa9..de212a13 100644 --- a/features/db-export.feature +++ b/features/db-export.feature @@ -147,3 +147,16 @@ Feature: Export a WordPress database When I try `wp db export --no-defaults --debug` Then STDERR should match #Debug \(db\): Running initial shell command: /usr/bin/env (mysqldump|mariadb-dump) --no-defaults# + + @skip-sqlite + @skip-windows + Scenario: Export database when PHP exec() is disabled + Given a WP install + + When I try `{INVOKE_WP_CLI_WITH_PHP_ARGS--ddisable_functions=exec} db export wp_cli_test.sql --porcelain` + Then the return code should be 0 + And STDOUT should contain: + """ + wp_cli_test.sql + """ + And the wp_cli_test.sql file should exist diff --git a/src/DB_Command.php b/src/DB_Command.php index 03b578af..75309042 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1,6 +1,7 @@ command_supports_option( $mysqldump_binary, 'column-statistics' ); /* * In case that `--default-character-set` is not given and `DB_CHARSET` is `utf8`, @@ -859,6 +860,19 @@ private function get_posts_table_charset( $assoc_args ) { return $stdout; } + /** + * Check whether a shell command advertises support for a specific option in `--help`. + * + * @param string $command Base shell command to inspect. + * @param string $option Option name to look for. + * @return bool Whether the option is listed in help output. + */ + private function command_supports_option( $command, $option ) { + $result = Process::create( "{$command} --help" )->run(); + + return false !== strpos( $result->stdout . $result->stderr, $option ); + } + /** * Imports a database from a file or from STDIN. * From 9e2586d1acb60ed3866cfa305d882df1e5698f96 Mon Sep 17 00:00:00 2001 From: swissspidy Date: Wed, 22 Apr 2026 08:45:46 +0000 Subject: [PATCH 46/53] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 4aadc6bc..bba7cddc 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -9,8 +9,12 @@ on: paths: - .github/workflows/copilot-setup-steps.yml +permissions: + contents: read + jobs: copilot-setup-steps: + name: Setup environment runs-on: ubuntu-latest permissions: contents: read @@ -18,6 +22,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Check existence of composer.json file id: check_composer_file @@ -36,6 +42,6 @@ jobs: - name: Install Composer dependencies & cache dependencies if: steps.check_composer_file.outputs.files_exists == 'true' - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4 + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 env: COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} From 4de3dfa1c1846241f1e046feffd3745d09211371 Mon Sep 17 00:00:00 2001 From: ernilambar Date: Thu, 14 May 2026 09:14:16 +0000 Subject: [PATCH 47/53] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index bba7cddc..d5e319e5 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -31,7 +31,7 @@ jobs: - name: Set up PHP environment if: steps.check_composer_file.outputs.files_exists == 'true' - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 with: php-version: 'latest' ini-values: zend.assertions=1, error_reporting=-1, display_errors=On From 0b47799953065df7534373d88a18dee63e5486d5 Mon Sep 17 00:00:00 2001 From: swissspidy Date: Tue, 2 Jun 2026 15:40:17 +0000 Subject: [PATCH 48/53] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index d5e319e5..617a49f0 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false From e2d3981dbcd87c463ce403d549bbca2912ac9ee9 Mon Sep 17 00:00:00 2001 From: swissspidy Date: Mon, 8 Jun 2026 16:57:25 +0000 Subject: [PATCH 49/53] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 617a49f0..ffb6f8fd 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -31,7 +31,7 @@ jobs: - name: Set up PHP environment if: steps.check_composer_file.outputs.files_exists == 'true' - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 with: php-version: 'latest' ini-values: zend.assertions=1, error_reporting=-1, display_errors=On From 2084d60f07e80bb02de4e84695e8825b73fbabab Mon Sep 17 00:00:00 2001 From: lelafa <63458648+lelafa@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:16:12 +0200 Subject: [PATCH 50/53] Add mysqldump flags docs link (#329) --- README.md | 2 +- src/DB_Command.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2aae83a2..52a2fed3 100644 --- a/README.md +++ b/README.md @@ -437,7 +437,7 @@ wp db export [] [--dbuser=] [--dbpass=] [--=] **Alias:** `dump` Runs `mysqldump` utility using `DB_HOST`, `DB_NAME`, `DB_USER` and -`DB_PASSWORD` database credentials specified in wp-config.php. Accepts any valid `mysqldump` flags. +`DB_PASSWORD` database credentials specified in wp-config.php. Accepts any valid [`mysqldump` flags](https://dev.mysql.com/doc/en/mysqldump.html#mysqldump-option-summary). **OPTIONS** diff --git a/src/DB_Command.php b/src/DB_Command.php index 75309042..984a1d60 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -636,7 +636,7 @@ public function query( $args, $assoc_args ) { * Exports the database to a file or to STDOUT. * * Runs `mysqldump` utility using `DB_HOST`, `DB_NAME`, `DB_USER` and - * `DB_PASSWORD` database credentials specified in wp-config.php. Accepts any valid `mysqldump` flags. + * `DB_PASSWORD` database credentials specified in wp-config.php. Accepts any valid [`mysqldump` flags](https://dev.mysql.com/doc/en/mysqldump.html#mysqldump-option-summary). * * ## OPTIONS * From b39251c848a611ce69f2d002d07603893f00b522 Mon Sep 17 00:00:00 2001 From: swissspidy Date: Thu, 18 Jun 2026 14:38:23 +0000 Subject: [PATCH 51/53] Update file(s) from wp-cli/.github --- .github/workflows/copilot-setup-steps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index ffb6f8fd..844ffe25 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false From 2ecf8a3eed5ec414399b5d63e6442dd33e268ba9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:02:11 +0200 Subject: [PATCH 52/53] Filter empty database parameters to prevent MySQL command failures (#308) Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Pascal Birchler --- features/db-check.feature | 32 ++++++++++++++++++++++++++++++++ src/DB_Command.php | 23 +++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/features/db-check.feature b/features/db-check.feature index 523ddd99..53c73d1a 100644 --- a/features/db-check.feature +++ b/features/db-check.feature @@ -162,6 +162,38 @@ 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-mysql-or-mariadb + Scenario: Empty DB credentials should not cause empty parameter errors + Given an empty directory + And WP files + + When I run `wp config create {CORE_CONFIG_SETTINGS} --dbcharset="" --skip-check` + Then STDOUT should not be empty + + When I run `cat wp-config.php` + Then STDOUT should contain: + """ + define( 'DB_CHARSET', '' ); + """ + + When I run `wp db create` + Then STDOUT should not be empty + + When I try `wp db check --debug` + Then the return code should be 0 + And STDOUT should contain: + """ + Success: Database checked. + """ + And STDERR should not contain: + """ + --default-character-set='' + """ + And STDERR should not contain: + """ + --default-character-set= + """ + @require-sqlite Scenario: SQLite commands that show warnings Given a WP install diff --git a/src/DB_Command.php b/src/DB_Command.php index 984a1d60..13de3c55 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1990,6 +1990,29 @@ private static function run( $cmd, $assoc_args = [], $send_to_shell = true, $int $final_args = array_merge( $required, $assoc_args ); + // Filter out empty string values to avoid passing empty parameters to MySQL commands + // which can cause errors like "Character set '' is not a compiled character set". + // However, keep empty strings for credential options like 'user' and 'pass' so that + // an explicitly empty value is not silently converted into an omitted parameter. + $final_args = array_filter( + $final_args, + static function ( $value, $key ) { + // Always drop null values. + if ( null === $value ) { + return false; + } + + // Preserve explicitly empty credential arguments. + if ( '' === $value && in_array( $key, [ 'user', 'pass' ], true ) ) { + return true; + } + + // For all other options, filter out empty strings. + return '' !== $value; + }, + ARRAY_FILTER_USE_BOTH + ); + // Adapt ordering of arguments. uksort( $final_args, From 161aa6b2b9d2d4467dc1b3e0bacebe775f57c13e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 6 Jul 2026 15:03:25 +0000 Subject: [PATCH 53/53] Regenerate README file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 52a2fed3..8ba809b8 100644 --- a/README.md +++ b/README.md @@ -1038,7 +1038,7 @@ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file ## Support -GitHub issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support +GitHub issues aren't for general support questions. For support resources and next steps, see the WP-CLI Support page: https://make.wordpress.org/cli/handbook/support/ *This README.md is generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). To suggest changes, please submit a pull request against the corresponding part of the codebase.*