Skip to content

fix(export): give a dump the TLS options its own client tool takes - #3049

Merged
datlechin merged 2 commits into
mainfrom
fix/backup-dump-tool-tls-options
Sep 22, 2026
Merged

datlechin merged 2 commits into
mainfrom
fix/backup-dump-tool-tls-options

Conversation

@datlechin

Copy link
Copy Markdown
Member

What happened

File > Backup Dump on a MySQL or MariaDB connection failed with:

/opt/homebrew/bin/mysqldump: unknown variable 'ssl-mode=PREFERRED'

NativeDumpRegistry.mysqlConnectionFlags built the TLS part of every mysqldump / mariadb-dump /
mysql / mariadb argument list from sslConfig.mode alone, in MySQL's spelling, for whichever
binary CLIExecutableFinder found on PATH. Three facts make that wrong:

  • The option surface belongs to the tool. MariaDB has no --ssl-mode at all.
  • The binary's name does not identify its flavor: Homebrew's mariadb formula installs MariaDB's
    dump tool as /opt/homebrew/bin/mysqldump, which is the first candidate TablePro tries.
  • No argument closure could branch on it anyway. NativeDumpDescriptor.Request carried no identity
    of the resolved tool, and resolveExecutable's (name, path) was handed to buildCommand as a
    sibling argument and dropped.

Measured

MariaDB 12.3.3 and MySQL 8.4.11 client tools, against a MariaDB server with TLS off, a MariaDB
server with a self-signed certificate, a MySQL server with its own, and PostgreSQL 17 with one.
Authenticating through MYSQL_PWD, which is how TablePro passes a password.

MariaDB tools MySQL tools
--ssl-mode=PREFERRED exit 7, unknown variable 'ssl-mode=PREFERRED' accepted
--ssl / --skip-ssl accepted exit 2, unknown option '--ssl'

The surfaces are disjoint, so the flags follow the tool:

SSL mode MySQL tools MariaDB tools
Disabled --ssl-mode=DISABLED --skip-ssl
Preferred --ssl-mode=PREFERRED --ssl --skip-ssl-verify-server-cert
Required --ssl-mode=REQUIRED --ssl --ssl-verify-server-cert
Verify CA --ssl-mode=VERIFY_CA --ssl --ssl-verify-server-cert
Verify Identity --ssl-mode=VERIFY_IDENTITY --ssl --ssl-verify-server-cert

plus --ssl-ca, --ssl-cert and --ssl-key on both, which nothing sent before.

Three measurements decided that table:

  • --ssl alone does not enforce TLS on MariaDB. With verification off it falls back to
    plaintext against a server without TLS and exits 0, which is the silent cleartext dump Required
    exists to prevent. Only --ssl-verify-server-cert refuses that server.
  • It is not stricter than Required, either. With no --ssl-ca given, MariaDB 12.3.3 accepted a
    server whose certificate said CN=totally.other.invalid. The flag requires TLS; --ssl-ca is
    what adds the identity check.
  • --loose-ssl-mode=PREFERRED is accepted by MariaDB and silently ignored. It looks like a
    one-character fix and downgrades the dump to plaintext.

The fix

NativeDumpResolvedTool (name, path, flavor, version text) is what resolution now produces, and
the argument closures take it as a second parameter instead of guessing. MySQLDumpToolIdentifier
classifies a binary from its --version, which carries the token through a rename because it comes
from the build rather than from argv[0]. MariaDB's own client names still answer when the tool
cannot be run at all, and a tool that stays unidentified is used for Disabled and Preferred and
refused for the three modes that promise encryption, rather than guessed at.

Four more defects in the same argument list, all found while tracing this one and all measured:

  • Verify CA could never dump, on MySQL or PostgreSQL. The CA, client certificate and key the
    connection carries reached neither tool. mysqldump --ssl-mode=VERIFY_CA with no --ssl-ca
    fails before connecting with error: 2026: CA certificate is required, and pg_dump with
    PGSSLMODE=verify-ca fails with root certificate file "~/.postgresql/root.crt" does not exist.
    Both now get the whole configuration, gated the way the live drivers already gate it.
  • A MySQL 8 tool against a MariaDB or MySQL 5.7 server wrote a partial file and exited 2, on
    Unknown table 'column_statistics' in information_schema (1109). It now passes
    --skip-column-statistics for exactly those servers, on the backup direction only.
  • A database whose name starts with a dash was dumped as a different database. The --
    terminator sat after the database name, so my_getopt still parsed it: measured on both
    families, a narrowed backup of a database named --no-data dumped database t1's table t2
    with no rows and exited 0, which the sheet reported as a successful backup. The terminator moved
    in front of the database name, in both directions.
  • The --version probe is shared. PostgreSQLDumpToolLocator's private spawn became
    CLIToolVersionProbe, so one probe serves both locators.

Reporting the failure

The same screenshot shows a second defect. The result sheet joined the destination folder and one
sentence per database into a single monospaced block, so /Users/Nick/Music/New/ above a database
called Music read as the one path /Users/Nick/Music/New/Music, which is how the reporter quoted
it. It also kept only the last line of the tool's output: for pg_dump that is
Is the server running on that host and accepting TCP/IP connections? while Connection refused is
thrown away, and for a role without SELECT it is detail: Query was: LOCK TABLE … while
permission denied for table … is thrown away.

The batch outcome is now a labelled Destination row and one row per database, following
CompareApplySheetView.outcomeRow: a state symbol with its own accessibility label, the database,
its size or Failed, and the failed row's whole output inline and selectable.

And ProcessNativeDumpRunner drains its stderr pipe before reading the buffer. The readability
source and the process reaper run on independent queues, so a tool that rejects an argument and
exits at once can leave its message in the pipe. Measured with a harness mirroring that class
against a child that writes 66 bytes and exits immediately: between 1 and 6 of every 300 runs
captured nothing at all
, the rate rising with load, and 0 of 1,200 with the fix. What the user saw
instead was Music failed: Process exited with code 7.

Two parts to it. A dedicated stderrLock is held across the read as well as the append, in the
readability handler and in the drain both, so a chunk can never be out of the pipe and still
missing from the buffer when the termination handler snapshots it: nil'ing readabilityHandler
does not join a callback already running. It is separate from stateLock, which cancel() takes
and which must never wait on a pipe. And the drain is a non-blocking read rather than
readDataToEndOfFile: the child has exited, so everything it wrote is already in the pipe's
buffer, while reading to EOF would take the same bytes and then wait for every writer to close,
which a grandchild that inherited this end would never do.

Three more, from the review of this branch

Codex's adversarial pass came back "do not ship" on the preflight this change introduces, and it
was right three times:

  • A Cancel taken before the tool ran was dropped. cancel() only acts once the state is
    .running, and start now spends two process spawns finding the binary and asking it what it
    is. A user who cancelled in that window saw the tool launch afterwards, which on a restore means
    writing to the database they had just said to leave alone. cancel() latches, and run refuses
    to start a runner once it is set.
  • The probe's deadline covered the process, not the read. readDataToEndOfFile waits for every
    writer to close, so a wrapper that prints its version, starts a helper inheriting standard output
    and exits leaves that EOF to the helper and the dump never starts. It reads what the pipe already
    holds instead, bounded.
  • The stderr drain was unbounded. It accumulated into an uncapped buffer and applied
    stderrByteCap only afterwards, so a surviving grandchild writing to inherited stderr could keep
    the loop fed and hold up the result and the credentials file's removal. The cap now applies per
    chunk and bounds the loop.

The last two are PostgreSQLDumpToolLocator's own code and this file's own drain; what puts them in
scope is that this change makes both run on every MySQL dump.

Before / After

Before is the reporter's own screenshot in #3046: two lines of one monospaced block, where
/Users/Nick/Music/New/ sits above Music failed: … and reads as a single path.

After is a labelled Destination row, then one row per database: a state symbol, the database,
its size or Failed, and a failed row's whole output inline underneath. I could not capture it from
a running build. The sheet only exists at the end of a real dump, the destination is an open panel
whose fallback is the user's own Downloads folder, and a second TablePro process would disturb the
test runs another session is running on this machine. #Preview("Backup Batch With A Failure") in
BackupResultSheet.swift renders exactly this case, the reporter's Music row included, so the
layout can be seen in Xcode without setting any of that up.

Verification

  • verify.sh build: PASS
  • verify.sh test over the eleven suites that own the changed types: PASS, 123 cases
  • verify.sh lint: PASS
  • docs/scripts/check-writing-style.sh and check-docs-against-source.py: PASS
  • scripts/check-mysql-dump-tool-flags.sh: new, and passing against both installed client families.
    It hands every flag in the table to a real tool of that flavor and fails on one the tool does not
    know. No server needed: a rejected flag fails before the socket opens, so a closed port tells the
    two apart.

No UI test. Producing this sheet deterministically needs a real dump to a chosen folder, and the
failed row needs a tool that fails on demand; the destination is an open panel with the user's own
Downloads as its fallback, which a UI test must not write into. The row model is a pure function
with unit coverage, including the reporter's exact case.

Fixes #3046

@mintlify

mintlify Bot commented Sep 22, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 22, 2026, 7:50 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@datlechin
datlechin merged commit b250f1d into main Sep 22, 2026
12 of 14 checks passed
@datlechin
datlechin deleted the fix/backup-dump-tool-tls-options branch September 22, 2026 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Backup dump fails - Error Message - unknown variable 'ssl-mode=PREFERRED'

1 participant