feat(connections): open a SQLite database that lives on an SSH server, read-only - #2553
Merged
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2474.
A SQLite connection can now name a database that lives on an SSH server. It is fetched over SFTP into a copy on this Mac, opened read-only, and the original is never written to.
Why this shape
buildEffectiveConnectionis the single place a transport rewrites what a driver sees: it swapshostandportfor a tunnel's local port and hands the driver a connection with no idea a tunnel exists. Every transport we ship is an arm of that one switch, but the switch only rewrites a network endpoint, and a file-backed driver opens a path. This adds the arm that rewrites the path. The driver learns nothing, exactly as it learns nothing about a tunnel.The pane's server half is the existing
ConnectionSSHTunnelView, so password, key, agent, keyboard-interactive, jump hosts, TOTP,~/.ssh/configand saved SSH profiles all work with no new code. What it adds is a path.Read-only is enforced, not promised
buildRemoteFileEffectiveConnectionsetssafeModeLevel = .readOnly. The grid, the SQL editor and the AI tools all refuse writes through the gate they already honour, rather than each learning what a remote file is.That is the honest shape rather than an unfinished one. An edit would land in the copy on this Mac, change nothing on the server, and vanish on the next fetch. SQLite's own documentation says its locking cannot be relied on across a network, so writing to the file in place is not on the table either.
Fetching safely
Where the server carries
sqlite33.27 or newer, it is asked for aVACUUM INTOsnapshot. That is the tier worth having: measured, a snapshot passedintegrity_checkwith 42,239 rows while 22,518 transactions committed against the source, with zero writer errors. A byte copy taken at the same moment was 441 rows behind.Otherwise the file is copied directly with any
-walor-journalbeside it. Those are not optional: measured, opening only the main file of a WAL-mode database returned the checkpointed row and silently omitted the committed one.-shmis deliberately not copied; it is a rebuildable index and a stale one is worse than none.Every download is verified before anything opens it, because
sqlite3_openon a truncated file reports success and shows an empty database. The byte count must match the server's stat, the header must be SQLite's, andintegrity_checkmust pass. Fetches land on a staging name and are promoted only after that.What the investigation turned up
Two things measured against the vendored
Libs/libssh2_arm64.aand a real OpenSSH 10.3 server changed the design. Neither is visible in the header, which carries no doc comments at all:libssh2_sftp_writeshort-writes constantly. 8 MiB, SHA-256 both sides: advancing the offset by the requested length wrote 4,224,304 of 8,388,608 bytes and returned no error at any point; advancing by the returned length was byte-exact after 511 short writes.The obvious rename API cannot replace a file. The header's own
libssh2_sftp_rename()macro passesOVERWRITE|ATOMIC|NATIVE, which are SFTP v5 flags; against OpenSSH's v3 the call fails withFX_FAILUREthe moment the destination exists. Onlyposix_rename_exreplaces a file.Neither is exercised by this PR, which never writes to a server.
scripts/check-sftp-contract.shmeasures both against a throwaway container so a libssh2 bump re-checks them rather than trusting this description.Two claims in the issue turned out to be wrong, both verified first-hand.
pathFieldRole == .filePathdoes not identify the file-backed types: it is SQLite and Beancount only, while DuckDB and libSQL declare.database/.apiOnlyand hide their path in an additional field. AndPluginMetadataRegistry.buildMetadataSnapshotrecords that reading a newDriverPluginstatic off an already-built plugin crashes withEXC_BAD_INSTRUCTION, so the capability is curated app-side instead. The result is that this PR changes no PluginKit API at all.Scope
SQLite only, and read-only.
DuckDB and libSQL open local files too and are deliberately excluded: DuckDB names its log
<database>.walrather than-wal, and both have local-versus-remote modes where the driver ignores a substituted path entirely, so routing a fetch to them would download a database nothing opens. Beancount is excluded because a ledger is a graph of files reached throughinclude.Write-back is out. Two adversarial review passes over an earlier, wider version of this change found the remaining defects concentrated there and in multi-engine handling: two connections sharing a working copy while one holds an unlinked inode, per-engine sidecar fingerprinting,
~/.ssh/configaliases making the storage key name the wrong server after a repoint. Those are worth doing properly rather than partly.Tests
LibSSH2SFTPSessionIntegrationTests, against a real OpenSSH server started byscripts/sftp-test-server.sh. Byte-exact 4 MiB download, awkward paths,realpathhome and symlink resolution, cancellation, directory refusal, missing-path naming, free space, directory listing. The suite is gated on a reachability probe and skips when nothing answers, becausexcodebuilddoes not pass the invoking shell's environment to the test host.Files are seeded on the server and verified against the server's own
sha256sum, so a passing download agrees with something this code did not produce.RemoteDatabaseFileTestsfor identity keying, fingerprint comparison including the write-ahead log, engine-specific sidecar and integrity dispatch, and the download verdicts (truncated, empty, non-database, WAL-mode, complete).RemoteFilePaneValidationTestsfor the pane that sharesSSHTunnelFormStatewith the SSH Tunnel pane.isFormValidreads every pane's issues whether or not it is visible, so an earlier version of this view model told every SSH-tunnelled MySQL and PostgreSQL connection it needed a remote database file path and disabled Save and Test on all of them. These pin that shut.No UI automation: the flow needs a live SSH server, so it is not deterministic under
TableProUITests.Verification
verify.sh buildPASSverify.sh testPASS across the eight suites aboveswiftlint --strictclean over every changed filedocs/scripts/check-writing-style.shandcheck-docs-against-source.pyboth passNot included
No remote file browser. The issue scoped it "if that is cheap to add", and a usable one needs paging, permission handling and a path bar.
No screenshots yet: the Remote File pane needs a reachable SSH server to photograph. Flagging rather than omitting,
docs/connections/remote-database-files.mdxcarries no<Frame>and should gain the light and dark pair before release.