refactor(storage): render joined UPDATEs through the dialect - #1009
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
morgo
left a comment
There was a problem hiding this comment.
🤖 Approving on Morgan's behalf (automated review, escalation rules apply).
All five joined-UPDATE conversions (IncrementEditCount, Supersede, ClearPendingFreeze, CompletePending, FailPending) render token-identical MySQL to the removed hand-written SQL — same ON condition, SET order, WHERE guards including the lease-token predicate — with argument order and placeholder counts (3/3/3/5/6) verified at every call site, so the RowsAffected-based lost-lease detection is unaffected. Dialect wiring is nil-safe (everything funnels through NewWithDependencies).
One non-blocking hardening suggestion: JoinedUpdate documents that joinCondition must not contain bind placeholders, but nothing enforces it — a future caller's stray ? would silently shift every subsequent binding by one, which in these queries means the lease token binds into the wrong slot. A strings.Contains(joinCondition, "?") panic, symmetric with the existing empty-assignments panic, would pin the contract.
Heads-up: once #1010's Postgres rendering exists, these lease-guarded joined UPDATEs render as UPDATE … FROM, which inherits the fencing question flagged on #1011 — one decision for the whole stack, nothing to change in this PR.
54f3ad2 to
8570883
Compare
Keep lease-guarded apply comment and control request updates portable across dialect-specific joined DML syntax. No behavior change for MySQL.
The join condition's render position differs per dialect, so the contract now forbids bind placeholders in it and defines the assignments-then-predicate argument order. Empty assignment lists panic at the seam instead of rendering invalid SQL.
41ca96f to
3a25477
Compare
There was a problem hiding this comment.
Pull request overview
Refactors sqlstore’s multi-table (joined) UPDATE statement rendering to go through the Dialect interface, keeping MySQL behavior byte-identical while enabling a future PostgreSQL dialect to render the same structured update as UPDATE ... SET ... FROM ... WHERE.
Changes:
- Adds
Dialect.JoinedUpdate(...)plusJoinedUpdateAssignmentand implements MySQL rendering inMySQLDialect. - Converts lease-guarded joined UPDATEs in apply-comment and control-request stores to call the dialect seam.
- Adds unit tests that pin the MySQL joined-UPDATE SQL strings (including placeholder ordering and empty-assignments panic).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pkg/storage/internal/sqlstore/storage.go | Wires Dialect into controlRequestStore so it can render joined UPDATEs via the seam. |
| pkg/storage/internal/sqlstore/dialect.go | Extends Dialect with JoinedUpdate and adds the MySQL implementation + assignment type. |
| pkg/storage/internal/sqlstore/dialect_test.go | Pins MySQL joined-UPDATE rendering and validates the empty-assignments panic behavior. |
| pkg/storage/internal/sqlstore/control_requests.go | Renders lease-guarded joined UPDATEs (complete/fail pending) through Dialect.JoinedUpdate. |
| pkg/storage/internal/sqlstore/apply_comments.go | Renders lease-guarded joined UPDATEs (edit count, supersede, clear pending freeze) through Dialect.JoinedUpdate. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…itions A stray placeholder in the join condition would bind into a dialect-dependent position and silently shift every subsequent binding, so the seam panics on it instead of trusting the doc contract.
The dialect qualifies assignment columns with the target alias itself, so a pre-qualified column would render an invalid double-qualified reference.
…nucleus * origin/main: refactor(storage): render joined UPDATEs through the dialect (#1009) fix(planetscale): hold the cutover when the operator defers it (#978) fix(observability): make telemetry resource schema-tolerant (#1014) # Conflicts: # pkg/storage/internal/sqlstore/dialect.go # pkg/storage/internal/sqlstore/dialect_test.go # pkg/storage/internal/sqlstore/storage.go
…-joined-dml-13l * origin/main: refactor(storage): render joined UPDATEs through the dialect (#1009) fix(planetscale): hold the cutover when the operator defers it (#978) fix(observability): make telemetry resource schema-tolerant (#1014) feat(postgres): implement declarative planning via pg-sprite diffplan (#1008) refactor(storage): make remaining sqlstore SQL dialect-portable (#1007) test(e2e): deflake multi-table stop/start resume and MySQL cold starts (#1005) ci: verify golangci config against a vendored schema (#997) feat(api): fail-closed verdict gating for postgres plans (#1004) feat(tern): route postgres targets to the postgres engine (#1003) feat(storage): stamp remaining sqlstore timestamps explicitly (#1006) # Conflicts: # pkg/storage/internal/sqlstore/apply_comments.go # pkg/storage/internal/sqlstore/dialect.go # pkg/storage/internal/sqlstore/dialect_test.go # pkg/storage/internal/sqlstore/settings.go # pkg/storage/internal/sqlstore/storage.go # pkg/storage/internal/sqlstore/updated_at_lint_test.go
…re-public-13n * origin/main: refactor(storage): portable lease-guarded joined DML for the operation store (#1011) fix(github): align lint warnings formatting with issues and fold long lists (#959) fix(github): lead with the database's operators on command-rejection comments (#960) docs: regenerate stale tables of contents (#968) fix(engine): heartbeat the row a local drive actually owns (#915) fix(github): scope auto-plan to the schema a pull request proposes (#1016) fix(vitess): dispatch task-less VSchema-only work operations over gRPC (#961) feat(storage): add PostgreSQL dialect nucleus to the shared store core (#1010) refactor(storage): render joined UPDATEs through the dialect (#1009) fix(planetscale): hold the cutover when the operator defers it (#978) fix(observability): make telemetry resource schema-tolerant (#1014) feat(postgres): implement declarative planning via pg-sprite diffplan (#1008) refactor(storage): make remaining sqlstore SQL dialect-portable (#1007) test(e2e): deflake multi-table stop/start resume and MySQL cold starts (#1005) ci: verify golangci config against a vendored schema (#997) feat(api): fail-closed verdict gating for postgres plans (#1004) feat(tern): route postgres targets to the postgres engine (#1003) feat(storage): stamp remaining sqlstore timestamps explicitly (#1006)
…lect-factory-14b * origin/main: feat(github): flag destructive changes to tables another open PR owns (#1017) feat(storage): add public postgresstore constructor (#1012) refactor(storage): portable lease-guarded joined DML for the operation store (#1011) fix(github): align lint warnings formatting with issues and fold long lists (#959) fix(github): lead with the database's operators on command-rejection comments (#960) docs: regenerate stale tables of contents (#968) fix(engine): heartbeat the row a local drive actually owns (#915) fix(github): scope auto-plan to the schema a pull request proposes (#1016) fix(vitess): dispatch task-less VSchema-only work operations over gRPC (#961) feat(storage): add PostgreSQL dialect nucleus to the shared store core (#1010) refactor(storage): render joined UPDATEs through the dialect (#1009) fix(planetscale): hold the cutover when the operator defers it (#978) fix(observability): make telemetry resource schema-tolerant (#1014) feat(postgres): implement declarative planning via pg-sprite diffplan (#1008) refactor(storage): make remaining sqlstore SQL dialect-portable (#1007) test(e2e): deflake multi-table stop/start resume and MySQL cold starts (#1005) ci: verify golangci config against a vendored schema (#997) feat(api): fail-closed verdict gating for postgres plans (#1004) feat(tern): route postgres targets to the postgres engine (#1003) feat(storage): stamp remaining sqlstore timestamps explicitly (#1006)
Summary
Moves the joined (multi-table) UPDATE statements in the apply comment and control request stores behind the storage
Dialectseam. MySQL's rendering is pinned byte-identical; a future PostgreSQL dialect renders the same structured parts asUPDATE ... SET ... FROM ... WHERE. No behavior change for MySQL.Why
The storage core is dialect-parameterized so a PostgreSQL backend can run the same store code. Multi-table UPDATE syntax is one of the few places MySQL and PostgreSQL diverge at the statement-shape level rather than the fragment level, so the seam hands the whole statement rendering to the dialect.
What
Dialect.JoinedUpdate(targetTable, targetAlias, joinTable, joinAlias, joinCondition, assignments, predicate)renders a full joined UPDATE. SET assignments are structured (JoinedUpdateAssignment{Column, Expr}) with unqualified target columns, and the join condition is passed separately from the residual predicate — the two constraints that make PostgreSQL'sUPDATE ... FROMshape expressible from the same inputs. The join condition must be placeholder-free (its render position differs per dialect); arguments bind assignments-first, then predicate.IncrementEditCount,Supersede,ClearPendingFreeze) and control requests (CompletePending,FailPending).Joined UPDATE/DELETEs in other store files (applies, operations, tasks) are deliberately out of scope — they follow in the next slices, and a joined-DELETE seam is added there with its first caller.
Before / after