Skip to content

fix(sidebar): give every Open Quickly item one Recent identity across runs, scopes and databases - #3082

Merged
datlechin merged 1 commit into
mainfrom
fix/quick-switcher-recent-identity
Sep 23, 2026
Merged

datlechin merged 1 commit into
mainfrom
fix/quick-switcher-recent-identity

Conversation

@datlechin

Copy link
Copy Markdown
Member

Found while investigating #3048 (#3060).

Open Quickly's Recent section and its frecency boost look items up by a string key in a per-connection store (QuickSwitcher.frecency.<connection> in UserDefaults). Three builders spelled that key differently, and one spelled it from a value that changes on every run.

The three defects

1. A recent query drops out of Recent once it runs again. A query-history row's key was its newest execution's UUID. Pick SELECT * FROM users from Open Quickly, run it, and the next panel lists the same statement under the new execution's UUID. The Recent entry points at an id nothing lists any more. The store handed back its ten newest ids before anything was matched against the list, so a handful of such picks emptied the All scope's Recent section.

2. A table is recent in every database of the connection. Tables were keyed table_<schema>.<name>, with no database, by both the switcher and the tab-open chokepoint (SharedSidebarState.commitTableOpen). Open public.users in app_prod, switch to app_staging, and public.users is in Recent, earns the frecency boost, and opens in app_staging. Schemas, routines, triggers and user types were keyed the same way.

3. The same table has two keys. The Connections scope keyed a table connection_<uuid>_<schema>.<name>_<TYPE>; the All and Tables scopes keyed it table_<schema>.<name>. Picks were split between the two, and each scope's Recent showed about half of its ten.

Root cause

The item's id did two jobs: SwiftUI row identity inside one list, and the persisted recall key. Each builder derived it from what it had at hand (an execution UUID, TableInfo.id which carries the type, or a schema and name without the database), and nothing made the builders agree.

The fix

One factory, QuickSwitcherFrecencyKey, spells every key, and every producer calls it: the All and Tables builder, the Connections builder, the Queries builder, and the tab-open chokepoint. QuickSwitcherItem stores frecencyKey; its id is derived from it, prefixed with the owning connection only when the item carries a target, so a list that mixes connections still has unique rows.

Kind Key
Table, view table_<schema>.<name>, escaped by IdentityPath from #3068. A table listed with no schema takes the schema its tab resolves to
Schema, routine, trigger, user type schema_…, routine_…, trigger_…, usertype_…
Any of the above on a connection that switches databases @<database>/ in front, the database escaped
Database db_<name>, unchanged
Saved query favorite_<uuid>, unchanged
History history_<SHA-256 of the trimmed statement>

The database qualifies a key only when the engine's supportsDatabaseSwitching is true. On a connection that reaches one database the database is a constant, so leaving it out keeps those keys byte-identical to what shipped. That also covers Redis, whose "tables" db0db15 are its databases and which reports no database switching.

The statement key is a hash because a history row can hold a statement of hundreds of kilobytes, and the store keeps up to 100 keys in UserDefaults.

Two changes the fix is not safe without:

  • Recent is capped after it is matched, not before. The store returns every key newest first, and the view model takes the first ten that the scope lists and that belong to the panel's connection. With the database in the key, the ten newest keys after a morning in app_prod all miss in app_staging, and capping first would empty Recent on every database switch. An entry that no longer resolves never holds a slot.
  • History collapses per connection. distinctByQuery kept one row per statement across every connection, so the Queries scope showed SELECT count(*) FROM events once, owned by whichever connection ran it last. With a stable key, that row's owner, and so its Recent entry, would flip each time the other connection ran it. It now keeps one row per connection and statement.

Recent and the frecency boost only count items owned by the panel's connection, which the old per-connection ids did implicitly. A replica with the same app.public.users is never this connection's Recent and never borrows its boost.

The Queries-scope builders moved to QuickSwitcherViewModel+QueryItems.swift, because the view model otherwise sat exactly on SwiftLint's 1,200-line limit.

What happens to stored entries

No migration and no code that reads both formats.

  • db_… and favorite_… keys are unchanged and keep resolving.
  • Tables, schemas, routines, triggers and user types on a connection that reaches one database (SQLite, libSQL, Oracle, Redis, BigQuery, Spanner and the other engines whose supportsDatabaseSwitching is false) keep their keys and keep resolving.
  • The same objects on a connection that switches databases were recorded without their database. Nothing in the record says which database, so mapping them would be a guess. They stay in the store, never match, take no Recent slot, and are pruned by the store's 100-entry cap.
  • history_<uuid> entries name one execution, not a statement. They age out the same way.
  • connection_… entries age out. Each was written beside a table_… entry for the same open, because opening from the Connections scope goes through the tab-open chokepoint, so they hold nothing the rules above do not cover.

User-visible result: once, after updating, Recent loses its query-history entries and, on connections that switch databases, its object entries. They come back as things are opened. The CHANGELOG says so under Changed.

Nothing outside the panel reads these keys. MCP's list_recent_tables reads RecentTablesStore, the sidebar's store, and AppleScript exposes neither.

Tests

QuickSwitcherRecentIdentityTests (new) drives the view model through each defect. QuickSwitcherItemIdentityTests pins that the chokepoint and both table builders produce one key for every table type, for a table listed with no schema, and for one listed under two kinds, plus the key rules above.

  • Build (verify.sh build): PASS.
  • Tests: QuickSwitcherRecentIdentityTests, QuickSwitcherItemIdentityTests, QuickSwitcherViewModelTests, QuickSwitcherCrossSchemaTests, QuickSwitcherHistoryItemTests, QuickSwitcherFrecencyStoreTests, QuickSwitcherCatalogStoreTests, IdentityPathTests, QueryTabManagerRecordingTests, QuickSwitcherOpenTableTests, SharedSidebarStateTests: 184 executed, 184 passed.
  • swiftlint lint --strict on every changed Swift file, run inside the branch's tree: clean.
  • Docs checks (verify.sh docs): PASS.

The edit that turns each defect's tests red:

  • Defect 1: key a history row by entry.id (queryStaysRecentAfterRerun, allScopeQueriesStayRecentAfterReruns), or collapse history across connections (statementOnTwoConnectionsKeepsBothRows).
  • Defect 2: make DatabaseQualifier ignore the database (tableIsRecentOnlyInItsDatabase, tabOpenIsRecentOnlyInItsDatabase, frecencyBoostStaysInItsDatabase), or cap Recent before matching (recentIsNotCrowdedOutByAnotherDatabase).
  • Defect 3: key Connections-scope tables with connection_<uuid>_<table.id> again (connectionsPickIsRecentInTablesScope, tenPicksAcrossScopesFillBothRecents, everyTypeAgrees).

Those mutations were worked out from the code and not run: the session hit its usage limit before a second build.

No UI test: the defects are in how keys are built and matched, which the unit tests drive through the view model's own Recent and ranking paths. A UI test would need two databases and a seeded query history in the sandbox, and would check nothing the view model tests do not.

Review

Codex is unavailable until Sep 29, so Skill(code-review) at high effort read the commit. Fixed:

  • A table listed with no schema was keyed without one while its tab records the resolved schema, so the two never met. Both builders now key the schema the tab resolves to.
  • One schema and name listed under two kinds (a stale per-schema list against a fresh flat one) gave two rows the same id. Both builders keep the first row per key.
  • Recent was resolved by building a dictionary of every scoped item, and the groups after it excluded Recent rows by building each row's id. It now ranks by the at most 100 stored keys and excludes by key and owner.
  • The new extension had no explicit access level.

Left, with the reason:

  • A table picked in Open Quickly is recorded twice, by the pick and by the tab open. The All and Tables scopes have done this since the chokepoint began recording, and the boost is relative, so every panel pick counts the same. Dropping the pick's record would lose picks that only switch to an open tab, which never passes the chokepoint.
  • A table opened before the session knows its schema is recorded without one, and resolveRecentSchema renames only the sidebar's Recent entry. That frecency entry never matches, and never matches the wrong table.
  • The Connections scope lists allLoadedTables without the loaded-database check the All scope's mergedTables applies, so for a moment after a database switch it can list the old database's tables under the new one. That predates this change and is a listing defect, not a key defect.
  • Computing id on read, and trimming then hashing a statement twice: both run over at most a few hundred rows per filter or per catalog build.
  • Always qualifying by database instead of passing connectionSwitchesDatabases: it would orphan every single-database connection's entries, and the database a single-database engine reports is only known after connect.

Docs

docs/features/open-quickly.mdx: the Ranking paragraph now says what Recent holds (tables opened outside the panel count too, which was already true) and that an object is recent only in its own database.

@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, 3:04 PM

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

@datlechin
datlechin merged commit f626e9a into main Sep 23, 2026
9 checks passed
@datlechin
datlechin deleted the fix/quick-switcher-recent-identity branch September 23, 2026 19:19

This branch was successfully deployed

1 active deployment
staging - docs 42aba620 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.

1 participant