feat(history): add query insights over the local history store - #2180
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. |
…ndow chrome (#2186) * fix(tabs): draw the editor tab strip as the system tab bar, in the window chrome Claude-Session: https://claude.ai/code/session_01D5BJ4TrCpMKvVQtQuxwwmH * fix(tabs): make the whole tab clickable again and stop the strip leaking observers Claude-Session: https://claude.ai/code/session_01D5BJ4TrCpMKvVQtQuxwwmH
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.
Summary
Adds Query Insights, a Starter-gated tab that summarizes the query history already stored on the user's Mac: most-run shapes, the ones costing the most time, the ones that got slower than the period before, the ones that fail, and an activity chart. Item 5 of #2107.
Everything is computed locally from
query_history.db. Nothing leaves the machine.The part that makes the numbers mean anything
Grouping by raw query text counts
WHERE id = 1andWHERE id = 2as two different queries, which makes every panel useless: the shapes you actually run hundreds of times each appear once. So this addsSQLQueryFingerprint, which normalizes a statement to its shape the waypg_stat_statementsand MySQL's statement digest do, and stores a digest of that shape in a new indexedfingerprint_hashcolumn.Measured, at the default 10,000-entry retention cap:
GROUP BYin SQLSo the fingerprint is computed once, on the write that already runs in a background
Task, and never on the read path. A hash column rather than a normalized-text column: same speed, +100 KB instead of +1.7 MB at 10k rows, and it does not copy query text into a second column, which matters given v3 deliberately droppedparameter_valuesfor that reason.SQLQueryFingerprintreuses the existingSQLTokenizerandSqlDollarQuote. Building it against the real tokenizer surfaced four defects a naive version would have shipped, each now covered by a test:SELECT $$secret payload$$ …leaked the literal into the shape; the tokenizer has no dollar-quote rule."..."was treated as a string literal. Outside MySQL it is a quoted identifier, soSELECT "email" FROM "Users"andSELECT "phone" FROM "Orders"collapsed into one group. Double quotes are now identifiers except on MySQL/MariaDB, where they are strings. Guessing wrong that way only splits a group; guessing wrong the other way merges two real tables and reports a wrong count.id=1andid = 1produced different shapes.IN (1,2)andIN (1,2,3)produced different shapes.Two things are deliberately not merged: identifier case (on a case-sensitive server
Ordersandordersare different tables) and numbers inside names (events_2025vsevents_2026).Thresholds, and why they are not guesses
A naive "got slower" panel reports noise. Against synthetic history with no real regression in it, a 1.2× threshold flagged 6 of 32 shapes, and raising the sample floor barely moved it. The ratio, not the sample count, is what separates a regression from variance. At 1.5× nothing false survived.
A regression therefore needs all of: 5 runs in both windows, a 1.5× increase, and a 25 ms absolute increase (a query going 1 ms → 2 ms has doubled without costing anything). Only successful runs count, since a query that failed fast is not one that got quicker.
Separately, ranking Slowest by average applies a 3-run floor, or one slow one-off statement sits at the top forever. That is the floor
pg_stat_statementsusers apply by hand when they sort onmean_exec_time.Other decisions worth flagging
localtimemodifier rather than dividing the epoch, so a query run at 22:00 in New York lands on the right day and a daylight-saving change does not shift a bucket. Verified against the system SQLite the app links.pg_stat_statementsidiom, and the query that actually costs you time), with a segmented control to switch to average. "Slowest" is genuinely ambiguous, so the panel lets the user say which they mean.Migration
fingerprint_hashis added and backfilled in a v4 migration. The column is added first and filled second, so an app killed between the two reopens with an unfilled column and finishes the backfill rather than stamping a version that claims work it did not do. The backfill is idempotent and covers both the upgrade-from-v3 and upgrade-from-v2 paths. Three tests cover the upgrade path directly.Testing
verify.sh build: PASSverify.sh test SQLQueryFingerprintTests QueryHistoryInsightsTests QueryHistoryFingerprintMigrationTests QueryHistoryStorageTests QueryHistoryMigrationTests QueryHistoryCaptureTests QueryHistoryEntryTests: PASSverify.sh lintoverTableProplus the three new test files and the UI test: 0 violations, docs references cleanQueryInsightsTabUITestscovers opening the tab from the Database menu, its singleton behaviour, and that its toolbar identifiers do not collide with the history drawer'sThe Pro gate decides how it looks, not what it does
requiresProapplies.disabledplus a scrim and nothing else, so it governs appearance only. Thefirst version of this view attached
.task { await viewModel.activate() }after it unconditionally,which meant an unlicensed Mac computed every aggregate, held a query-history subscription for the
whole session, and left the resulting numbers in the view hierarchy where the accessibility tree
could read them straight through the blur. Activation is now gated on the same answer the scrim uses,
keyed with
.task(id:)so activating a license mid-session still starts it.QueryInsightsTabUITestscovers it: the test sandbox carries no license, so that run sees exactlywhat an unlicensed user sees, and it asserts none of the four panels is built.
Before / After
This adds a screen that did not exist, so there is no before. The after is the Query Insights tab: a summary row, an activity chart, and the four ranked panels.
I could not capture a truthful screenshot of it, and I would rather say so than post a misleading one. Three things have to line up at once: the
demo/databases running, Screen Recording granted to whatever drivesscripts/export-screenshots.sh, and an active Starter license in the keychain. Without the license the tab renders behind the.requiresProscrim, so a capture would photograph the upgrade prompt rather than the feature. Without a populated history every panel renders its empty state.Say the word and I will drive a licensed Debug build against the demo databases and post the real shots here and into
docs/images/.Known gaps
docs/images/query-insights.pngand-dark.pngare generated stand-ins at the same 1560×960 the other shots use, so the page renders rather than showing a broken image. A real capture needsscripts/export-screenshots.shwith thedemo/databases up, Screen Recording granted, and an active Starter license, since an unlicensed run would photograph the paywall overlay. Please replace before release.Localizable.xcstringsis deliberately not in this PR. The working tree had that file modified by another session (Entra ID strings), and the new strings here will be picked up the next time the catalog is regenerated.Related
Closes the Query Insights item in #2107. Supersedes the approach in #2111, which grouped by raw query text.