fix(storage): canonicalize lock and check identity keys - #1216
Conversation
MySQL's ai_ci collation deduplicates mixed-case identity keys through idx_locks_database and idx_checks_check_key; PostgreSQL compares byte-wise, so the same event stream can double-book locks or duplicate check rows. Fold identity args Go-side at the store boundary (writes and predicates, including the lock-intent guard) as a backstop behind ingress canonicalization, with cross-dialect parity subtests.
There was a problem hiding this comment.
Pull request overview
This PR strengthens cross-dialect correctness by canonicalizing (lowercasing) storage identity keys at the SQL store boundary so MySQL’s case-insensitive collation behavior is matched by PostgreSQL’s byte-wise comparisons. It prevents duplicate rows and missed predicates for locks and stored check state when callers provide mixed-case repository/database/environment/type values.
Changes:
- Add
storage.CanonicalKey()and apply it to lock/check identity fields on writes and on query predicate arguments in the SQL stores. - Canonicalize lock intent verification predicates (
verifyExpectedLockIntent) to avoid case-sensitive mismatches. - Add cross-dialect storagetest subtests asserting mixed-case write + differently-cased read behaves identically on MySQL and PostgreSQL.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/storage/canonical.go | Introduces CanonicalKey helper for folding identity key strings. |
| pkg/storage/internal/sqlstore/locks.go | Canonicalizes lock identity fields on Acquire/Update and canonicalizes database/type/repo predicate args on reads/deletes. |
| pkg/storage/internal/sqlstore/checks.go | Canonicalizes check identity fields on upserts/state transitions and canonicalizes key predicate args on reads/queries/deletes. |
| pkg/storage/internal/sqlstore/applies.go | Canonicalizes lock-intent guard query args for database/type. |
| pkg/storage/storagetest/locks.go | Adds canonical-identity storagetest covering mixed-case lock write and case-insensitive lookup/release behavior. |
| pkg/storage/storagetest/checks.go | Adds canonical-identity storagetest covering mixed-case check upsert and case-insensitive lookup/query/delete behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Acquire is now the one fold point on its call chain (internal helpers no longer refold), canonicalizeLock gains the same nil guard as its twin, and the in-place identity canonicalization contract is documented on the store godocs. Addresses external review of pull/1216.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
🤖 Adversarial correctness review, requested by @aparajon and performed by their agent. Reviewed at head Verdict: folding at the store boundary is the right layer and the conformance tests are the right shape, but folding writes without folding what is already stored splits stored check state in two on PostgreSQL, and the one mutual-exclusion primitive that is byte-wise on both dialects is left unfolded two lines from a fold this PR adds. I reproduced the first against real MySQL and PostgreSQL. Reviewed alongside #1213 and #1214, which are independent branches off the same commit and ship a byte-identical copy of Findings1. There is no backfill, so on PostgreSQL every stored check row written before this deploy goes invisible and the next upsert creates a second row for the same logical key.
2. 3. The doc comment justifies excluding lock owners on a premise that is not true. 4. The new conformance cases hold 5. Two of the eight tables that carry identity columns fold. Action items
Verified (tried to break, couldn't)The fold placement inside the store is right and I could not find a read path that skips it: This review was generated by Claude Code (claude-opus-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Approving on @aparajon's behalf after the adversarial correctness review above. The findings there are yours to pick up as follow-ups — flagging them, not gating on them.
This stamp was left by Claude Code (claude-opus-5).
The sha256-derived advisory lock name is byte-sensitive on both dialects, so case drift in database, type, or environment would defeat mutual exclusion across differently-spelled callers.
|
🤖 Review response — created by Kiran's code review agent (Amp, Claude Opus 4.5) — pull/1216, follow-up commit Response to Armand's adversarial review. The lock-name hash fold is a genuine catch and is fixed here; the backfill remains sequenced as the series' final PR. Severity-ordered.
|
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
MySQL's accent- and case-insensitive default collation let a differently-cased owner release or touch a lock it does not hold, diverging from the byte-wise ownership contract the acquire path and PostgreSQL already enforce.
…dcolumn-ddl-seam * origin/main: (28 commits) docs: document the PostgreSQL support envelope (#1144) fix(engine): report why a Vitess schema change failed (#1242) feat(ddl): detect statements whose cost scales with table size (#1237) fix(operator): keep a multi-table apply running while tables are queued behind a cutover (#1241) fix(storage): index the webhook inbox claim ordering (#1196) fix(github): drop the cutover duration promise from progress surfaces (#1240) fix(github): render row-copy progress percentages at their true precision (#1239) fix(observability): do not report a shutdown as a claim failure (#1233) fix(github): tell an operator why a refused apply's database is busy (#1224) fix(engine): do not mark an apply failed when its driver shuts down (#1234) feat(github): render live row-copy progress on sharded table lines (#1191) feat(ui): add approximate row and byte formatters (#1236) fix(planetscale): delete the branch an apply created when it fails before its deploy request (#963) feat(api): app grouping field on database config (#1226) feat(cli): filter pulled tables with --table (#1235) fix(github): refuse a Vitess foreign key at plan time instead of at apply time (#966) feat(lint): add severityglyphs analyzer to keep the severity vocabulary in pkg/glyph (#1153) feat: remove the volume control operation end to end in favor of autoscaling (#1225) ci: give the k8s e2e job budget room for setup plus go test's timeout (#1232) fix(storage): canonicalize lock and check identity keys (#1216) ... # Conflicts: # pkg/ddl/parser.go # pkg/ddl/parser_test.go
Fold lock and check identity keys Go-side at the store boundary as a backstop behind ingress canonicalization.
Why
MySQL's ai_ci collation deduplicates mixed-case identity keys through
idx_locks_databaseandidx_checks_check_key; PostgreSQL compares byte-wise, so the same event stream can double-book a database lock or duplicate check rows, and release/cleanup predicates can miss existing rows. With canonical ingress these folds are no-ops in practice — they defend against future non-canonical callers.What
locks.goandchecks.go: identity fields (repository, database name, database type, environment) folded on written values and query-predicate args in every method.verifyExpectedLockIntent) folds its identity args the same way.LOWER()(keeps indexes usable).Before / after