Skip to content

feat(sidebar): version history for saved queries and Git status for linked SQL folders - #3076

Open
datlechin wants to merge 1 commit into
mainfrom
feat/saved-query-version-control
Open

datlechin wants to merge 1 commit into
mainfrom
feat/saved-query-version-control

Conversation

@datlechin

Copy link
Copy Markdown
Member

Summary

Version history for saved queries, and Git status, history and Discard for files in a linked SQL folder.

Fixes #2505

What was missing

A saved query was one row in sql_favorites.db that every edit overwrote, and nothing in the app knew about Git. A linked folder inside a repository showed no status, no diff and no way back to an earlier version.

Saved queries

  • A favorite_versions table, filled by SQLite triggers on favorites. When a query's SQL changes, the trigger stores the text it replaced and keeps the newest 50 for that query. Deleting the query deletes them.
  • The triggers sit on the table, so every writer is covered: the edit dialog, a restore, and the sync upsert (INSERT … ON CONFLICT DO UPDATE fires UPDATE triggers, measured on SQLite 3.54). An edit lost to a sync echo (a pre-existing bug, reported separately) now leaves a recoverable version.
  • The table is device-local. No CloudKit record type or field changes, so nothing needs a Production schema deploy.
  • favorite_query_times holds when each query's SQL was last written, backfilled from updated_at for existing queries, so a rename or keyword change does not move the date shown on the next version.
  • Inside a trigger, INSERT OR REPLACE takes the outer statement's conflict policy, and under the sync upsert that aborted the whole write (measured, and caught by a test). The trigger bodies delete then insert instead.
  • The version schema is created after migrateIfNeeded(): the v1→v2 migration renames and drops favorites, which would drop triggers created before it.
  • Restoring runs one UPDATE … SET query, updated_at and marks the query for sync. The text it replaces becomes a version itself, so a restore can be undone the same way.

Linked SQL folders in a Git repository

  • Finding git. Candidate paths are checked with isExecutableFile and never run to probe: Homebrew, the developer directory read from /var/db/xcode_select_link, the Command Line Tools, then Xcode. /usr/bin/git and xcrun are never used, because on a Mac without the Command Line Tools the shim opens the installer.
  • Running git. One argv prefix on every call: --no-optional-locks --no-pager --literal-pathspecs, core.fsmonitor=false, core.hooksPath=/dev/null, color.ui=never, log.showSignature=false, safe.bareRepository=explicit. Inherited GIT_DIR-style variables are stripped. GIT_OPTIONAL_LOCKS=0, GIT_TERMINAL_PROMPT=0 and GIT_NO_LAZY_FETCH=1 are set. status does not rewrite .git/index (inode and mtime checked in a test).
  • Status letters. status --porcelain=v2 -z plus ls-files -z --full-name, run from the linked folder and mapped through rev-parse --show-prefix. An ignored file shows no letter and gets no Git commands. Every u record counts as a conflict, including AA.
  • Refresh. The snapshot includes HEAD, so an amend or rebase that leaves every file status alone still refreshes an open history tab. Status refreshes on folder rescans, on an FSEvents stream over each repository's absolute git directory (this also covers a linked folder nested inside a larger repo), and when the app becomes active. SQLFolderWatcher no longer rescans every SQL file when an event only touches .git.
  • History. log --follow -z --name-status keeps the path each commit had, so a version reads correctly across renames. A commit that deleted the file is skipped, since it has no version to show. Commit ids that are not 40 or 64 hex characters are dropped before they can reach a cat-file argument, which also carries --end-of-options.
  • Restore and Discard never let git write the file. Both read the raw blob (cat-file blob <commit>:<path>, or :./<file> for the index) and write it with the same atomic write the editor's save uses. So no smudge or process filter runs on a user action, and the bytes written are the bytes the preview showed. A Git LFS pointer is refused with a message.
  • Plan, confirm, write. The file's bytes are captured before any prompt, and the write only happens if they are unchanged. "Has uncommitted changes" compares bytes with HEAD as well as reading status, so files marked assume-unchanged or skip-worktree still ask first. Discard restores the index version, so staged work is kept, and it re-reads the index at the moment of writing so a staging change during the prompt aborts the write.

One History tab for both

  • A new TabType.versionHistory, restored on relaunch the way the object-source tab is. The version list uses FieldDrivenList, next to Changes (split or unified diff) and Content (the read-only ObjectSourceView).
  • The diff runs off the main thread once per selection. Only past versions are cached; the current one is re-read every time. Inputs over 5,000 lines show content instead of a diff, and text that differs only in line endings reads as unchanged.
  • Refresh (Cmd+R) reloads the tab.
  • TextDiffView is lifted out of StructureDefinitionDiffView, which now renders through it, rather than adding a fourth diff renderer.

Not gated behind Pro: Linked SQL Folders and saved queries are free today.

Before / After

There is no before for the History tab: it is new. Before this change the sidebar row for a linked file showed no status letter and its menu had no Git commands; that state was not captured separately.

After, a linked file with uncommitted changes (the top row compares HEAD with the file on disk; M and U letters in the sidebar):

History tab on the working copy, with M and U letters in the sidebar

After, a commit selected in dark mode, compared with the current version:

History tab comparing a commit with the current version, dark mode

Security

Automatic git status in a linked repository runs that repository's own clean filters, as git status in Terminal would. The docs page carries a warning to link only trusted repositories. Nothing in this change runs a smudge filter, hook, fsmonitor, pager, external diff, textconv or signature program. A security review built a repository whose config wires up each of those and ran the exact commands against it: only the documented clean filter fired, and only during status.

Verification

  • Build: PASS.
  • Unit tests:
    • storage triggers: record, skip, prune per query, cascade, upsert, restore, v1 upgrade, query save time;
    • git parsers: byte formats measured from real git;
    • locator: never returns the shim;
    • argv hardening;
    • status mapping: conflicts, ignored files;
    • view model: baseline selection, caching, confirm-at-write, failed refresh;
    • tab persistence and window title;
    • menu spec;
    • UTF-32 and UTF-16 decoding;
    • Refresh routing.
  • Integration tests against scratch repositories (skipped when no git):
    • linked subfolder prefix;
    • index untouched;
    • rename history;
    • restore across a rename;
    • Discard keeps staged content;
    • a changed file refuses the write;
    • assume-unchanged still asks;
    • unborn repository;
    • not a repository.
  • UI test SavedQueryHistoryUITests: create a saved query, edit it, Show History, restore.
  • Lint and the docs checks pass.
  • Reviews:
    • Codex review (two passes) and adversarial review: every finding fixed except the pre-existing sync-acknowledgement bug, reported separately.
    • Security review: no findings.
    • SwiftUI review: applied.

@mintlify

mintlify Bot commented Sep 23, 2026

Copy link
Copy Markdown

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

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 23, 2026, 8:12 AM

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

This branch was successfully deployed

1 active deployment
staging - docs f14ef7c1 Deployed Sep 23, 2026 by mintlify[bot]
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.

Version control for saved queries

1 participant