fix(sidebar): give every Open Quickly item one Recent identity across runs, scopes and databases - #3082
Merged
Conversation
… runs, scopes and databases
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
This branch was successfully deployed
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.
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 usersfrom 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). Openpublic.usersinapp_prod, switch toapp_staging, andpublic.usersis in Recent, earns the frecency boost, and opens inapp_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 ittable_<schema>.<name>. Picks were split between the two, and each scope's Recent showed about half of its ten.Root cause
The item's
iddid 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.idwhich 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.QuickSwitcherItemstoresfrecencyKey; itsidis 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.table_<schema>.<name>, escaped byIdentityPathfrom #3068. A table listed with no schema takes the schema its tab resolves toschema_…,routine_…,trigger_…,usertype_…@<database>/in front, the database escapeddb_<name>, unchangedfavorite_<uuid>, unchangedhistory_<SHA-256 of the trimmed statement>The database qualifies a key only when the engine's
supportsDatabaseSwitchingis 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"db0…db15are 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:
app_prodall miss inapp_staging, and capping first would empty Recent on every database switch. An entry that no longer resolves never holds a slot.distinctByQuerykept one row per statement across every connection, so the Queries scope showedSELECT count(*) FROM eventsonce, 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.usersis 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_…andfavorite_…keys are unchanged and keep resolving.supportsDatabaseSwitchingis false) keep their keys and keep resolving.history_<uuid>entries name one execution, not a statement. They age out the same way.connection_…entries age out. Each was written beside atable_…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_tablesreadsRecentTablesStore, the sidebar's store, and AppleScript exposes neither.Tests
QuickSwitcherRecentIdentityTests(new) drives the view model through each defect.QuickSwitcherItemIdentityTestspins 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.verify.sh build): PASS.QuickSwitcherRecentIdentityTests,QuickSwitcherItemIdentityTests,QuickSwitcherViewModelTests,QuickSwitcherCrossSchemaTests,QuickSwitcherHistoryItemTests,QuickSwitcherFrecencyStoreTests,QuickSwitcherCatalogStoreTests,IdentityPathTests,QueryTabManagerRecordingTests,QuickSwitcherOpenTableTests,SharedSidebarStateTests: 184 executed, 184 passed.swiftlint lint --stricton every changed Swift file, run inside the branch's tree: clean.verify.sh docs): PASS.The edit that turns each defect's tests red:
entry.id(queryStaysRecentAfterRerun,allScopeQueriesStayRecentAfterReruns), or collapse history across connections (statementOnTwoConnectionsKeepsBothRows).DatabaseQualifierignore the database (tableIsRecentOnlyInItsDatabase,tabOpenIsRecentOnlyInItsDatabase,frecencyBoostStaysInItsDatabase), or cap Recent before matching (recentIsNotCrowdedOutByAnotherDatabase).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:id. Both builders keep the first row per key.id. It now ranks by the at most 100 stored keys and excludes by key and owner.Left, with the reason:
resolveRecentSchemarenames only the sidebar's Recent entry. That frecency entry never matches, and never matches the wrong table.allLoadedTableswithout the loaded-database check the All scope'smergedTablesapplies, 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.idon read, and trimming then hashing a statement twice: both run over at most a few hundred rows per filter or per catalog build.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.