feat(backup): add File > Backup Dump… and Restore Dump… for PostgreSQL (pg_dump / pg_restore)#1211
feat(backup): add File > Backup Dump… and Restore Dump… for PostgreSQL (pg_dump / pg_restore)#1211samirmhsnv wants to merge 11 commits into
Conversation
Picks a database from the current connection, prompts for a destination, and runs pg_dump with the custom archive format (-Fc), surfacing live byte progress and a cancel that cleans up the partial file. Reuses the active SSH tunnel via session.effectiveConnection so tunneled connections work without spinning up a second tunnel. pg_dump path is auto-detected from PATH and common Homebrew locations, with an override under Settings > Terminal > CLI Paths.
Picks a .dump file via NSOpenPanel, then a target database on the current PostgreSQL or Redshift connection, then runs pg_restore --no-owner --no-acl with progress and cancel. Reuses the active SSH tunnel via session.effectiveConnection. pg_restore path is auto-detected with a custom override under Settings > Terminal > CLI Paths. The backup progress and result sheets gain a Kind enum so both flows share them. DatabaseSwitcherSheet gains a .restore mode alongside .backup; both hide the schemas tab and create/drop affordances and hand the selection to the parent flow without auto-dismissing.
|
hey @datlechin please take a look when you available. thx |
|
Hello @samirmhsnv, thank you for contributing. This PR look great, could you add some screenshots about this feature for me preview first? |
…Dump / Restore Dump
Sure! Take a look screenshots below: 1. Two new options: Backup Dump and Restore Dump
2. Backup dump window:
3. Save Location popup
4. Progress bar & Complete message
5. Restore window to choose dump
6. Restore window to choose DB, with current one auto choosed
7. If any errors on restore:
|
|
Thanks @samirmhsnv, few things blocking this:
|
Folds PostgresBackupService and PostgresRestoreService into a single PostgresDumpService with a Kind enum (.backup / .restore). One state shape, one error shape, one stderr cap, one SSL switch, one termination handler. Other review-driven changes: - Inject a `PostgresDumpRunner` so the state machine can be exercised in tests without launching real subprocesses; `ProcessPostgresDumpRunner` is the production runner. - Stderr now accumulates entirely off the main actor inside the readabilityHandler closure (NSLock-guarded), with a single MainActor hop carrying the final string back through the termination continuation. - Cancel alert reads "Keep Backing Up" / "Keep Restoring" instead of "Continue". - NSOpenPanel for restore now has allowedContentTypes = [.dump, .data] and drops the "directory format" claim (canChooseDirectories is false). - Unified flow: both file panels are now sub-sheets of the SwiftUI sheet. Restore opens NSOpenPanel from inside the sheet on first appearance (with a Change… link in the source banner) instead of pre-opening before the sheet exists. - Real progress bar for backup: queries pg_database_size up front and feeds it as totalBytes; BackupProgressSheet renders a determinate bar capped at 95% (the dump file is smaller than the database thanks to compression). Tests: command construction (args/env/SSL/host fallback/password) and state machine transitions (success, failure, cancel, double-run guard, empty-stderr fallback) via a FakeDumpRunner. Plugin boundary move (item 1 of the review) is deferred to a follow-up commit. That change requires a PluginDatabaseDriver method, a plugin kit ABI bump, and Info.plist updates across every plugin, and is large enough to deserve its own review.
|
@datlechin Thanks for the review. So
Agreed in principle. Deferred to a follow up commit on this branch, the change requires adding a method to PluginDatabaseDriver, bumping currentPluginKitVersion 11→12, and updating TableProPluginKitVersion in ~14 plugin Info.plists (every bundled and externally distributed driver). That's mechanical but large enough that I'd rather not bundle it with the refactor below. Will push as a separate commit if you want it before merge; otherwise filing as a follow up.
Done. PostgresBackupService + PostgresRestoreService folded into one PostgresDumpService with a PostgresDumpKind enum. One state shape (PostgresDumpState), one error shape (PostgresDumpError), one stderr cap, one SSL switch, one termination handler. The two old files are deleted.
Added TableProTests/Database/PostgresDumpServiceTests.swift, 6 command construction tests (-Fc/--no-owner shape, host fallback, username omission, password optionality, SSL mode mapping) and 5 state-machine tests (success, failure with stderr surfaced, cancel, double run guard, empty stderr fallback) using a FakeDumpRunner. To make the state machine testable, factored run(command:database:fileURL:totalBytesEstimate:) as a separate seam so tests skip singleton lookups.
Fixed. now reads "Keep Backing Up" / "Keep Restoring" via a per kind keepGoingLabel.
Fixed. Open panel now sets allowedContentTypes = [UTType("dump"), .data]. Dropped the "directory format" wording from the message canChooseDirectories stays false and the message now reads "Select a backup file produced by pg_dump in custom archive format (.dump)."
Fixed. Both flows now open their NSPanel as a sub sheet of the SwiftUI sheet: Backup: sheet → DB picker → NSSavePanel as sub sheet → progress (unchanged)
Looked at this carefully, current code on both services already reads effective.host.isEmpty ? "127.0.0.1" : effective.host (both arms reference effective). There's a hostFallback test asserting this in the new suite. If you were looking at an earlier paste of the file, please point me at the line you saw and I'll recheck.
Fixed. BackupDatabaseFlow queries SELECT pg_database_size(...) via the live driver before invoking the runner and passes the value through totalBytesEstimate. BackupProgressSheet renders a determinate ProgressView(value:) capped at 95% (the .dump custom format is heavily compressed so the bar would otherwise look "done" while pg_dump is still finalizing the trailer). Restore stays indeterminate as you'd expect.
Fixed. The readabilityHandler now accumulates entirely inside the closure under NSLock, with no actor hop. The single MainActor handoff happens once on termination, carrying the final string back via withCheckedContinuation. The 64KB cap is applied off actor too. |







Summary
Two new File menu items for PostgreSQL and Redshift connections:
pg_dump -Fc(custom archive format) with live byte progress and cancel..dumpfile via the open panel, pick a target database on the current connection, runspg_restore --no-owner --no-aclwith indeterminate progress and cancel.Both flows reuse the connection's active SSH tunnel via
session.effectiveConnection, so tunneled connections work without spinning up a second port forward. Both honor user-set binary paths under Settings > Terminal > CLI Paths forpg_dumpandpg_restore; if unset, paths are auto-detected fromPATHand common Homebrew locations.Notes for reviewers
DatabaseSwitcherSheetgains aModeenum with three cases:.switch(default),.backup,.restore. The existing switch-database call site stays on the default and is unchanged. In the two handoff modes the schemas tab and create/drop affordances are hidden, the primary button reads "Backup Dump…" or "Restore Dump…", and the callback does not auto-dismiss so the host flow can transition to the next step.DatabaseSwitcherViewModelnow accepts an optionalinitialModeoverride. The two handoff modes force.databaseso PostgreSQL doesn't open the picker in schema mode (pg_dump / pg_restore operate on whole databases, not individual schemas).BackupProgressSheetandBackupResultSheetare shared between both flows via aKindenum. Cancel alerts and titles switch per kind. The cancelled-restore result warns about partial database state.ActiveSheetgains.backupDatabaseand.restoreDatabase(fileURL: URL)(the latter carries the chosen file so the sheet can open directly on the database picker).MainContentCommandActions.backupDatabase()and.restoreDatabase()are the menu entry points. The restore action opensNSOpenPanelfirst as a sheet on the main window, then sets the active sheet — this keeps file selection out of the SwiftUI sheet stack.PostgresBackupServicepolls file size every 250ms for live byte progress.PostgresRestoreServiceis indeterminate (pg_restore has no usable progress hook for arbitrary archives). Cancel sends SIGTERM in both. Stderr is captured (capped at 64KB) and surfaced as the failure message.PGSSLMODEvalues (require,verify-ca,verify-full, etc.) instead of being passed through verbatim.--no-passwordis passed to both tools so they fail fast with an auth error rather than hanging on a TTY prompt whenPGPASSWORDis missing.pg_restoreis invoked with--no-owner --no-aclto avoid spurious failures restoring as a different role from the dump owner.--cleanis intentionally not used; restoring into an existing database with conflicting objects will produce stderr errors and a non-zero exit, surfaced in the result sheet.xcodebuildfrom CLI fails on a SwiftLint plugin sandbox issue inCodeEditTextView/CodeEditSourceEditor(unrelated to this change). SwiftLint--strictis clean on every changed file. Verify the build from Xcode.Test plan
Backup Dump
.dumpfile is a valid custom-format archive (pg_restore -l file.dump).lsof).Restore Dump
Both