feat(compare): match changed queries by shape across a hash change (#3367)#177
Merged
Conversation
…3367) Run comparison keyed only on the exact query hash, so adding a column to a SELECT — which changes the hash but not the query's shape — showed the same call site as one `removed` + one `new` query instead of one changed query. Real dogfood: Nutcracker's `findByUser` gained two columns and read as removed+new, which also made the find_regressions rows noisy. Fall back to a shape match when the exact hash misses. The shape key is core's `normalizedFingerprint`, which strips bare column references from every SELECT list while keeping FROM/WHERE/ORDER/LIMIT/aggregates — so a column-add pairs with its previous self and inherits the cost delta, while a genuinely different query does not. Matching is two-pass (all exact-hash matches reserved before any shape match) so a new query can't steal a previous one a later query matches exactly, and requires the same sqlcommenter call site when both are tagged so identically-shaped queries from different sites aren't merged. Analyzer-only: the key is computed on the fly from the query text both runs already carry — no payload/API change, and no dependency on the edit-stable symbol provenance (#3494), since the file tag is stable here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Query Doctor Analysis
3 queries analyzed
0 regressed · 0 improved · 0 new · 0 removed
2 pre-existing issues
SELECT "guests"."id", "guests"."session_id", "guests"."username", "guests"."avatar_path", "guests"."color", "guests"."side", "guests"."audio_recording_path", "guests"."audio_recording_public", "gue...
indexassets(event_id, inserted_at desc)
cost 31,003,449 → 1,498 (100% reduction)SELECT * FROM guest_ip_addresses WHERE ip_address = '127.0.0.1';
indexguest_ip_addresses(ip_address)
cost 154,402 → 8 (100% reduction)
Using assumed statistics (10000000 rows/table). For better results, sync production stats.
More detail → get_ci_run({ runId: "019f5eaf-5f2c-72f1-aeb3-ca08884ba7d1" }) · view run · docs
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.
What
compareRunsmatched the previous run only by exact query hash. Adding a column to a SELECT changes the hash but not the query's shape, so the same call site showed up as oneremoved+ onenewquery instead of one changed query.This adds a shape-match fallback: when the exact hash misses, pair the current query with a previous one of the same shape and call site.
Why — Nutcracker dogfood (#3367)
findByUsergained two SELECT columns:af6f56c6…—SELECT id, share_slug, user_id … FROM design_renders WHERE user_id=$1 ORDER BY created_at DESC LIMIT $2(staging,postgres.ts:1755)96698339…— same query +session_id, source_payload(PR,postgres.ts:1761)Same call site, reported as removed+new — and that churn is also what made
find_regressionsnoisy.How
normalizedFingerprint, which strips bare column refs from every SELECT list (top-level, CTEs, subqueries, set-ops) while keeping FROM/WHERE/ORDER/LIMIT/aggregates. So a column-add produces the same key; a WHERE/table/aggregate change does not. Computed on the fly from thequerytext both runs already carry — no payload/API change.filetag, require the same origin, so identically-shaped queries from different sites aren't merged.Scope
Analyzer-only. Does not need the edit-stable symbol provenance from #3494 — the
filetag is stable in this case; only line numbers moved, and the shape key doesn't depend on them. #3494's symbol axis is still what a cross-commit / drifted-worktree join would need; this isn't that.Tests
query-shape.test.ts— column-add keeps the key; WHERE/table change moves it; unparseable → null; origin-compatibility rules.site-api.test.ts— column-add reads as one changed query (not new+removed); carries the cost delta; different call site not merged; widened variant is new when the original still runs; genuinely different query not matched.makeQuerynow gives each hash distinct SQL (real queries can't share text without sharing a hash).Full suite green (299), typecheck clean.