fix: run the SSL-mode migration from the --mcp server process (#639) - #643
fix: run the SSL-mode migration from the --mcp server process (#639)#643aesslinger wants to merge 6 commits into
Conversation
Moves stale_postgres_ssl_mode_replacement, migrate_connection_ssl_mode_in_place, and their tests out of commands.rs into a new connection_migrations module. The orchestration function (migrate_postgres_ssl_mode_spelling) is split into a path-based core (migrate_postgres_ssl_mode_spelling_at_path, no AppHandle dependency) plus a thin AppHandle wrapper left in commands.rs that just resolves the path and invalidates the connection cache. Per .rules/rust.md rule 1 (extract pure helpers into dedicated sibling modules) and to avoid a one-directional dependency between commands.rs (6300+ lines) and mcp/mod.rs — this is prep for wiring the migration into the --mcp server process (issue TabularisDB#639), which has no AppHandle. That wiring is a separate follow-up commit; this one is a pure extraction with no behavior change, except the return type improves from Result<()> to Result<bool> so callers can tell "nothing needed migrating" apart from "migrated and saved."
Adding this migration to the --mcp process (next commit) makes it the first write path that process exercises against connections.json — previously read-only there. MCP clients spawn tabularis --mcp as an independent subprocess that can run at the same time as the (single-instance) GUI app, and this repo's connection persistence layer has no file locking anywhere. Without a guard, the GUI saving an unrelated edit (e.g. a rename) at the same moment this migration is mid read-modify-write could be silently discarded: the migration reads before the GUI's edit lands, computes its rewrite on stale data, then overwrites. Compares the file's raw content immediately before writing; if it changed since this function's own initial read, skips the write for this run rather than clobbering the concurrent edit. Safe to skip because the migration is idempotent and self-terminating — the next process to load connections (GUI reopen, or MCP's next startup) will see the still-stale value and retry. Content comparison (not mtime) is used because some filesystems have coarse (1-second) mtime resolution that wouldn't reliably detect two writes landing within the same second. Considered and rejected: real OS-level file locking (the correct fix, but would need to touch ~20 existing write call sites across commands.rs to achieve actual mutual exclusion, not just this one path — worth its own issue); an atomic temp-file-plus-rename write (fixes torn writes on a crash, not the lost-update race this guards against); and refusing TLS at connection time instead of migrating the saved value (sidesteps the race entirely but changes behavior, not just structure — a bigger conversation than this fix). A compare-and-swap on file content is proportionate to how narrow and self-limiting the actual risk is: at most 2 concurrent writers, one-time and self-terminating once migrated, and the MCP call site only runs once at process startup, not per request.
…abularisDB#639) register_drivers_for_mcp() never called any connection migration — not the new SSL one, and not the pre-existing SSH one either — so a connection saved with a stale ssl_mode value before TabularisDB#614's dropdown fix shipped stayed silently cleartext forever when accessed via `tabularis --mcp`, even after the GUI-side fix landed, as long as it was never reopened in the GUI to trigger get_connections's own migration call. Appended as the last step of register_drivers_for_mcp(), after plugin loading completes — the migration resolves each connection's driver dialect through the same driver registry, so it needs plugin drivers already registered, same ordering constraint the GUI path already has via its .setup() block_on. Errors are swallowed the same way the GUI call site already does (.ok()): migration failure must never block MCP server startup. migrate_ssh_connections has this same gap but is meaningfully more complex — it touches a separate ssh_connections.json file and OS keychain credential migration. Left as a follow-up decision, not fixed here.
migrate_postgres_ssl_mode_spelling_at_path read connections.json twice before comparing content — once directly for content_before, then again inside persistence::load_connections_file. Not a correctness bug (traced every ordering: a concurrent edit landing in the gap between the two reads still gets caught by the final content_before vs content_now comparison, since content_before was captured first and content_now last), just an unnecessary extra syscall and a slightly wider window than needed. Splits load_connections_file's parsing logic into a new persistence::parse_connections_file(&str), so a caller that already has the file's content in hand can parse it without a second read. load_connections_file itself is unchanged in behavior — still reads then delegates to the new function — so none of its ~20 other call sites need to change. Found during PR review (TabularisDB#643).
Local verification notesRan a local review pass against Rules compliance — all satisfied: pure helpers extracted to a dedicated sibling module ( Blast radius — full grep of every moved/new function name confirms nothing outside the touched files references them; no external callers missed. One nuance found and fixed: Manual end-to-end verification: built the real
Second run produced no No blockers found. All test/build/lint claims in the PR description were independently reproduced, not taken on faith. |
| // --- migrate_postgres_ssl_mode_spelling_at_path: path-based core --- | ||
| // | ||
| // These exercise the file-level behavior (missing file, no-op file, | ||
| // save-and-report-true) without touching the driver registry — the |
There was a problem hiding this comment.
SUGGESTION: The comment lists save-and-report-true as one of the file-level behaviors these tests exercise, but no test in this block actually drives migrate_postgres_ssl_mode_spelling_at_path through its write path (the Ok(true) return after save_connections_file). Only the two no-op cases are present. The dialect-resolution logic is well covered by the migrate_connection_ssl_mode_in_place tests above, but the path-based function's own happy path (rewrite to disk + return Ok(true)) and its concurrent-change skip branch (connection_migrations.rs:162-170) have no integration coverage here — connections_file_changed_concurrently is unit-tested in isolation, but its wiring into the path-based function isn't. Either add a test that seeds a stale ssl_mode and asserts the file is rewritten + Ok(true) (a driver-id other than "postgres" would require a registered driver, which is why these tests avoid it — but a fixture/registered driver would close the gap), or correct the comment so it doesn't overstate coverage.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Incremental review of commit Files Reviewed (1 file)
Previous Review Summary (commit 6a556c7)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 6a556c7)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (6 files)
Reviewed by glm-5.2 · Input: 46.1K · Output: 7.5K · Cached: 156K |
Claimed a "save-and-report-true" case existed for migrate_postgres_ssl_mode_spelling_at_path; only the two no-op paths are actually tested. Corrects the comment to say what's covered and, more usefully, what isn't and why (the write-success path and the concurrent- change skip branch both need a driver registered under a non-"postgres" id, which needs either a live plugin process or a 62-method DatabaseDriver mock — judged disproportionate, not an oversight). Found by the Kilo Code review bot on PR TabularisDB#643.
Closes #639
Summary
The stale
ssl_mode-spelling migration added in #614 only ran from the two GUI-process Tauri commands that load connections (get_connections/get_connections_with_groups). It was never invoked from the standalonetabularis --mcpserver process, which reads the sameconnections.jsonand usesssl_modedirectly to establish real database connections. A connection saved with a stale MySQL-stylessl_modevalue before #614's dropdown fix shipped stayed silently cleartext forever via MCP, even after the GUI-side fix landed, as long as it was never reopened in the GUI.What changed
Four commits, each independently buildable/testable:
refactor: extract SSL-mode migration into a path-based shared module— moves the migration logic out ofcommands.rsinto a newconnection_migrations.rsmodule. The orchestration function is split into a path-based core (migrate_postgres_ssl_mode_spelling_at_path, noAppHandledependency) plus a thinAppHandlewrapper left incommands.rsfor the GUI's cache-invalidation need. Pure extraction, no behavior change — except the return type improves fromResult<()>toResult<bool>so callers can tell "nothing needed migrating" apart from "migrated and saved."fix: guard the SSL-mode migration against a concurrent-write race— this fix makes MCP a writer ofconnections.jsonfor the first time (previously read-only there). Since MCP clients spawntabularis --mcpas an independent subprocess that can run alongside the (single-instance) GUI app, and this codebase's connection persistence layer has no file locking, adds a content-based compare-and-swap: the file's raw content is checked immediately before writing, and the migration skips itself for this run (rather than overwriting) if it changed since the initial read. Safe because the migration is idempotent and self-terminating — the next process to load connections will retry.fix: run the SSL-mode migration from the --mcp server process— wires the shared core intoregister_drivers_for_mcp(), appended after plugin loading completes (same ordering constraint the GUI path already has, since dialect resolution depends on the driver registry). Errors are swallowed the same way the existing GUI call site already does — migration failure must never block server startup.refactor: avoid a redundant file read in the SSL-mode migration guard— found during local review: the guard readconnections.jsontwice before comparing (once directly, once again insidepersistence::load_connections_file). Not a correctness bug — traced every ordering and a concurrent edit landing in the gap still gets caught by the final comparison — but an unneeded extra syscall and a slightly wider window than necessary. Splitsload_connections_file's parsing logic into a newpersistence::parse_connections_file(&str)so the migration can parse the content it already read, without touching any of that function's ~20 other call sites.Explicitly out of scope
migrate_ssh_connectionshas this same gap (never called from MCP either) but is meaningfully more complex — a separatessh_connections.jsonfile plus OS keychain credential migration. Left as a follow-up decision, not fixed here.Full design rationale, alternatives considered, and why each was rejected are documented on #639 before implementation began.
Verification
cargo build --all-targets— 0 warningscargo clippy --all-targets— 0 findings in touched files (129 pre-existing elsewhere, confirmed present on a cleanmaincheckout)cargo fmt --check— clean on all touched filescargo test --lib— 1138/1138 (4 pre-existing environment-only skips)cargo test --test postgres_integration --include-ignored— 181/181POSTGRES_PLUGIN_BINagainst a livev1.0.0-beta.3binary) — 83/83tabularis --mcpbinary and ran it against an isolated$HOMEwith the real installed plugin binary and a hand-writtenconnections.jsoncovering all 3 driver cases. Confirmed the exact expected outcome: thepostgresql-driven connection's stalerequiredrewrote torequire(with the[Migration] Rewriting stale ssl_mode spelling on 1 postgres-dialect connection(s)log line), whilemysqland builtinpostgresconnections were left untouched. Second run confirmed idempotent (no log line, no further changes).