Stops a host option file overriding the credentials you configured. Recommended for anyone whose machines carry a ~/.my.cnf.
-
A password in
~/.my.cnfsilently beat the one db-tools was given, and the backup that failed as a result reported success. MySQL's client tools read option files before anything passed to them, and the precedence between the three ways to supply a password is command line > option file > environment. This tool puts the user on the command line and the password inMYSQL_PWD— the right place for it, since it stays out ofps, and also the weakest of the three. On a machine with credentials in an option file the connection was therefore attempted with the configured user and somebody else's password, and refused.The symptom was a 13-byte archive and
Backup created. v3.2.3 fixed the reporting half of that, so the failure is now visible; this fixes the cause. Every command that shells out tomysql,mysqldumpormysqlchecknow passes--no-defaults, which has to be the first argument or MySQL ignores it without saying so.Only when a password is configured. Passing it unconditionally would break the case where an option file is the intended source of credentials and none was configured on purpose — socket authentication, or a
~/.my.cnfan operator maintains. Nothing is being overridden there, so nothing is stopped.mysqlbinlogis untouched: it reads local files here and never authenticates.
DBTOOLS_ALLOW_HOST_DEFAULTS=1reads option files as before.--no-defaultsalso drops non-credential tuning such asmax_allowed_packetanddefault-character-set, and a host that keeps those in an option file needs a way back. It re-enables the credential clash above, so it is an informed choice rather than a default.- Tests pinning both directions — the flag appears with a password and stays away without one — its position, the escape hatch including the values that do not open it, and that the dump command itself carries it. The last of those is the one that matters: a helper that is correct and one command builder still bypassing it would pass every other test and fix nothing.
Fixes a deadlock that made streaming import and restore hang forever instead of reporting why they failed. Recommended for everyone.
-
A failing pipeline stage could hang the whole command indefinitely, swallowing the error that caused it.
runPipeline()handed each intermediate pipe's read end to the next stage as its stdin but never closed its own copy. The kernel therefore still counted a reader on that pipe, so when the finalmysqlstage exited early — bad credentials, refused connection, wrong database — the upstreamzstdnever receivedEPIPE. It blocked on a full 64KB buffer,gpgblocked behind it, and the poll loop spun forever. Meanwhilemysql's error sat undrained in a pipe that was only read after every process had exited, which now never happened.The reported symptom was
importof a.sql.zst.gpgarchive running 55 minutes on Ubuntu, creating zero tables, printing nothing beyond the progress spinner, and having to be killed. The archive was fine and the error was real — the tool discarded it. The same command now fails in under a second with the stage's own message, for examplePipeline failed at stage 2 (mysql --host=…): ERROR 1045: Access denied. -
Pipeline output was collected only after all stages exited, so any stage noisy enough to fill a pipe buffer deadlocked. A pipe holds roughly 64KB; a stage that writes more than that to stderr blocks on the write until something reads. Both
runPipeline()and the backup path'srunPipelineToFile()read only once every stage had finished, which for such a stage could not happen. Both now drain stdout and stderr non-blockingly inside the poll loop. Retention is capped per stage, but draining continues past the cap, and each pass is bounded so one loud stage cannot starve the others or stall the progress spinner. -
The reported failure named the wrong stage. When a stage died, every stage upstream took
SIGPIPEon its next write, and the exit-code check threw on the first failing stage by index — surfacingBroken pipefromgpgwhile burying the reason the pipeline actually stopped. Failures that are merely collateral from a downstream death are now dropped whenever a stage failed on its own terms, and the message names the stage it came from. -
gpg's passphrase could reach the console and the logs. The fallback error message rendered the failing stage's full argument list, which for a decrypt or encrypt stage includes--passphraseand the archive's key. The value is now redacted.
- Regression tests for each deadlock: a last stage that fails immediately while an upstream stage still has megabytes to write, a stage flooding stderr past the buffer, and a final stage flooding stdout that nothing consumes. Each runs the pipeline in a child process under a hard timeout, so a regression fails the build rather than hanging CI forever. Also a test that a failing stage's description cannot leak the passphrase.
- The
--pinentry-mode loopbackquestion was revisited and the v3.2.2 conclusion holds. Decryption with--batch --passphraseand no loopback was re-tested headless, with no TTY andpinentry-program /usr/bin/false, against GnuPG 2.2.19, 2.2.27, 2.4.4 and 2.5.21: it succeeds on all of them. The documented loopback requirement applies to operations that pull secret-key material throughgpg-agent; this tool only uses symmetric encryption, which takes the passphrase directly. It was not the cause of the hang above and the flag would not have prevented it.
Fixes a data-loss bug in restore. Recommended for anyone using --skip-safety-backup.
-
restoredropped the target database before checking that the archive existed.recreateDatabase()issuesDROP DATABASE IF EXISTS/CREATE DATABASE, and the archive validation ran after it. A mistyped archive path therefore destroyed the database and only then reportedArchive not found, leaving nothing to restore from. The same ordering applied to an encrypted archive with no usable passphrase. Both are now validated before anything destructive happens.With the default safety backup this was recoverable — a pre-restore archive had already been written. With
--skip-safety-backupit was not. Verified against a live server: the same command that previously left the target database empty now leaves it untouched.
- Tests for the destructive paths, which were the thinnest-covered part of the codebase: that the safety backup runs by default and is skipped only when asked; that the
DROPnames the database the caller requested and not the one baked into the archive filename; and that validation precedes it. For PITR: that the recovery target and database filter reach every binlog, that binlogs are applied in order, that meta-file resolution joins the recorded directory, and that explicit binlogs override the meta file.
--pinentry-mode loopbackwas considered forgpgand deliberately not added. Testing on GnuPG 2.2 and 2.4, headless and with no pinentry program available, showed the existing--batch --passphrasesymmetric flow works without it.
Fixes --version reporting the wrong release, plus bugs surfaced by adding static analysis.
db-tools --versionreported a stale release. The version was a hardcoded literal inConsoleFactorythat was never bumped, so it still read3.0.2after both the 3.1.0 and 3.2.0 releases. Anyone checking on a server whether a fix had landed would have seen an older version than was actually installed and reasonably concluded the deploy had not reached that box. It is now read from Composer viaInstalledVersions, so it always matchescomposer show amitdugar/db-toolsand cannot drift again. A test pins it to that source.importandrestorereported a nonsense duration when a failure occurred early:$startwas assigned inside thetry, so anything thrown before it left the variable undefined and the catch block computed elapsed time from nothing.restoreandimportcould pass a null passphrase togpgas a command argument. Unreachable through the current callers, which check first, but it now fails with a clear message rather than depending on a caller-side invariant.verifyandpitr-infowere constructed with a config argument their constructors do not accept. PHP silently discards extra arguments, so this was inert, but it hid the fact that neither command supports profiles.- Replaced
preg_split('/\t/', …)withexplode("\t", …)in the row parsers.preg_splitcan returnfalse, and the results were passed straight tocount(), which is a fatal in PHP 8. canUseFzf()passedshell_exec's result totrim()without handling thefalseit returns on failure.- Removed dead code: a reference to
BackupOptions::$tempDir, a property that has never existed, so theisset()guarding it was always false; an unusedENV_MAPPINGSconstant duplicating.envhandling that already works elsewhere; and a stale docblock oncreateBackupDirectory()documenting a different method's signature.
- PHPStan at level 8, running in CI on both PHP 8.4 and 8.5. Pre-existing cosmetic findings are captured in
phpstan-baseline.neonso the build fails on anything new; the bugs listed above were fixed rather than baselined.
DbTestCommand::__construct()no longer takes aRuntimeInterfaceas its first argument, andSetupCommand::__construct()no longer takes aProfilesConfig; neither was used. These are internal command classes wired byConsoleFactory, so this affects nothing that goes through the CLI or the service interfaces — but code constructingDbTestCommanddirectly needs to drop the first argument.
Fixes a silent backup-corruption bug and adds proper MariaDB support. No BC breaks.
- Backups could silently succeed while producing an incomplete archive. The dump pipeline was run as a shell string, so its exit status reflected only the last stage. A
mysqldumpthat died — bad credentials, server gone, disk full mid-dump — was invisible, because the compressor downstream succeeded on the truncated input and exited 0. The backup was reported as successful and a valid-looking, incomplete archive was written. The pipeline now runs underpipefailso a failure in any stage fails the backup. If no pipefail-capable shell is found, a warning is logged. - Every backup and restore failure was masked by an unrelated fatal. The cleanup paths in
BackupService::runPipelineToFileandProcessRunner::runPipelinecalledproc_close()on handles they had already closed.@does not suppress the resultingTypeError, so the real error — bad credentials, agpgfailure, a missing file — was replaced by an uncaught fatal and a stack trace. Notably this made--encryption-passwordappear broken whengpgfailed for environmental reasons: no archive, no usable error. backupnow creates its output directory if missing, matchingexport. Previously a non-existent--output-dirfailed with an error naming neither the directory nor the cause.collationno longer picksutf8mb4_0900_ai_cifor MariaDB. The old check wasversion_compare(VERSION(), '8.0.0', '>='), which is true for MariaDB 10.x and 11.x, but only MariaDB 11.4+ knows that collation — on 10.5, 10.6 and 10.11 everyALTERaborted withERROR 1273 Unknown collation.db:testreported every MariaDB server as having0suptime. It readperformance_schema.global_status, and MariaDB ships withperformance_schemaoff, so the value came backNULL. It now usesSHOW GLOBAL STATUS, which works on both.db:testno longer bypassesProcessRunnerwith a rawproc_open, and drops a deadsizeService()call.
- MariaDB is now detected from
VERSION()rather than inferred from the version number, including the5.5.5-compatibility prefix that MariaDB 10.x reports for old clients.db:testshows the detected server, andcollationpicks its default accordingly. - Client binaries resolve automatically between the classic and MariaDB names (
mysqldump/mariadb-dump, and so on), since MariaDB 11.0 renamed them and deprecated themysql*symlinks. Override any of them withDBTOOLS_MYSQL_BIN,DBTOOLS_MYSQLDUMP_BIN,DBTOOLS_MYSQLBINLOG_BINorDBTOOLS_MYSQLCHECK_BIN. CollationService::serverVersion()exposes the detected flavour and version. Deliberately not added toCollationServiceInterface, so third-party implementations keep working.
CollationServiceInterface::isMysql8OrHigher()is unchanged in signature and now returnsfalsefor MariaDB. Marked@deprecatedin favour ofserverVersion().- A MySQL 8.4 client cannot authenticate against a MariaDB server at all — they no longer share an auth plugin. Use the MariaDB client; the fallback above picks it up when installed.
- Honor an explicit
encryption_passwordas a fixed backup passphrase. When a password is supplied (via--encryption-passwordor a profile'sencryption_password), it is now used verbatim and no random token is embedded in the filename, so the archive is portable across machines whose database password differs. Restore opens it with the same--encryption-password. - Backward compatible: with no explicit password the default scheme is unchanged —
DB_password+ a random token embedded in the filename, auto-derived on restore. The restore path is unchanged.
- CI now runs the PHPUnit test suite on PHP 8.4 and 8.5 (previously only validated and linted).
- Bump
actions/checkoutto v5 (clears the Node 20 deprecation warning). No library code changes.
- Release automation: tag pushes now publish to Packagist automatically via CI. No library code changes since 3.0.0.
Breaking: drops support for PHP 8.2 and 8.3, and moves to the Symfony 8 stack.
- BC break: require PHP 8.4+ (tested on 8.4 and 8.5). The
phpconstraint is now^8.4, aligned withsymfony/console ^8.1which requires PHP 8.4.1. - BC break: upgrade
symfony/processto^8.1andamitdugar/archiveutilto^2.0. - Upgrade PHPUnit to
^13.1(dev). - Test against PHP 8.4 and 8.5 in CI (dropped 8.2).
- Stream backup/restore/import operations to eliminate temporary file disk usage.
- Ported backup/restore/verify/collation/export/clean/PITR/binlog purge services from legacy script into PSR-4 classes.
- Added real runtime wiring with Symfony Console commands for all operations.
- Integrated ArchiveUtility for compression/decompression and password-protected ZIP handling.
- Added PHPUnit test suite covering service flows and option parsing.
- Documented usage, requirements, licensing, and package metadata for Packagist release.