feat(storage): add PostgreSQL dialect nucleus to the shared store core - #1010
Conversation
Move INSERT IGNORE, JSON boolean predicates, and the index hint behind the dialect; rewrite TIMESTAMPDIFF and SUBSTRING_INDEX portably. No behavior change for MySQL.
Keep lease-guarded apply comment and control request updates portable across dialect-specific joined DML syntax. No behavior change for MySQL.
PostgreSQL binder ($n rebind aware of strings, identifiers, dollar quotes, and line comments), upsert/timestamp/interval rendering, RETURNING-id identity insertion, and the first cross-dialect parity lanes (settings, apply logs) plus pgx value-contract pins. Renders the full dialect seam: ON CONFLICT DO NOTHING inserts, null-safe jsonb boolean predicate, empty index hint, and UPDATE ... FROM joined updates with SET placeholders ordered ahead of the predicate.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
PostgreSQL has no automatic timestamp renewal, so the settings upsert now stamps updated_at explicitly and the heartbeat-stamp lint also scans dialect JoinedUpdate call sites. Rebind no longer miscounts ordinals around block comments or identifier-embedded dollar signs, JSON path keys follow the plain-identifier panic contract, and integration tests pin the lease-guarded RETURNING path and plain timestamp round-trips.
morgo
left a comment
There was a problem hiding this comment.
🤖 Approving on Morgan's behalf (automated review, escalation rules apply).
Verified the risk areas: every ON CONFLICT target has an exactly matching unique index in pkg/schema/postgres (so PG's stricter named-target requirement can't throw); the ?→$n rebinder correctly skips escaped strings, quoted identifiers, $$/tagged dollar quotes, and both comment styles in a single left-to-right pass so ordinals stay aligned with argument order; RETURNING id identity insertion mirrors MySQL's RowsAffected==0 guarded-insert semantics; de-pinning tasks/applyLogs/planComments/settings from the hardwired MySQLDialect is identity-preserving for MySQL; and the PG integration tests genuinely run in CI against postgres:16 (integration job passed, not skipped).
Non-blocking findings:
- Nothing pins
timezone=UTCon PG sessions — server-sidenow()cast into the schema's plaintimestampcolumns goes through session TimeZone, so lease-expiry/staleness predicates would skew on a non-UTC server, and the postgres:16 container's UTC default means this lane can't catch it. Suggest pinningtimezone=UTCin the DSN in the public-constructor follow-up (#1012). - The PR body's "MySQL behavior is unchanged" slightly overclaims: settings.Set's upsert now stamps updated_at on a same-value write where the old ODKU left the row untouched. Deliberate, documented in-code, and covered — just worth a wording fix.
- Rebind's dollar-quote scanner accepts digit-leading tags (
$1$…$1$) that the PG lexer would treat as a parameter; unreachable with the store's?-only corpus.
See the fencing note on #1011 — the PostgresDialect JoinedUpdate introduced here is the rendering in question.
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
…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.
…k' into kiran01bm/pg-backend-nucleus * origin/kiran01bm/sqlstore-joined-dml-13k: refactor(storage): reject bind placeholders in JoinedUpdate join conditions refactor(storage): tighten the JoinedUpdate dialect contract refactor(storage): render joined UPDATEs through the dialect 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
…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
There was a problem hiding this comment.
Pull request overview
Adds a PostgreSQL dialect “nucleus” to the shared pkg/storage/internal/sqlstore core, implementing PostgreSQL placeholder rebinding, SQL rendering seams, and identity insertion behavior so the same store logic can run against PostgreSQL, plus initial integration parity tests.
Changes:
- Introduces
PostgresDialectimplementing rebinding (?→$n) and dialect SQL seams (upserts, joined UPDATE, JSON boolean predicate, relative time, etc.). - Adds PostgreSQL identity insertion via
RETURNING id, including guarded-insert semantics. - Adds first PostgreSQL integration parity lanes for Settings + ApplyLogs and pgx stdlib value contract tests; updates the updated_at stamping lint to account for dialect-rendered
JoinedUpdatecall sites.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pkg/storage/internal/sqlstore/updated_at_lint_test.go | Extends the source-scan lint to also validate updated_at stamping in dialect-rendered JoinedUpdate call sites. |
| pkg/storage/internal/sqlstore/storage.go | Ensures all store families receive injected dialect/identity dependencies (removes hardwired MySQLDialect{} in a few stores). |
| pkg/storage/internal/sqlstore/settings.go | Clarifies updated_at stamping expectations for upserts across dialects. |
| pkg/storage/internal/sqlstore/postgres_integration_test.go | Adds PostgreSQL integration parity tests (Settings + ApplyLogs) and pgx stdlib value contract assertions. |
| pkg/storage/internal/sqlstore/identity.go | Adds PostgreSQL identity insertion using RETURNING id, including guarded-insert no-row handling. |
| pkg/storage/internal/sqlstore/dialect.go | Implements PostgresDialect (rebinding, upserts, joined UPDATE rendering, etc.). |
| pkg/storage/internal/sqlstore/dialect_test.go | Adds unit tests covering PostgresDialect rebinding and SQL seam renderings. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The PostgreSQL lexer reads a digit-leading run like $1 as a parameter placeholder, never a dollar-quote delimiter, so the rebinder must not skip placeholder rewriting between $1$…$1$ pairs. Unreachable with the store's ?-only corpus; hardening for parity with server tokenization.
…-joined-dml-13l * origin/main: fix(vitess): dispatch task-less VSchema-only work operations over gRPC (#961) feat(storage): add PostgreSQL dialect nucleus to the shared store core (#1010) # Conflicts: # pkg/storage/internal/sqlstore/dialect.go # pkg/storage/internal/sqlstore/dialect_test.go # pkg/storage/internal/sqlstore/postgres_integration_test.go # pkg/storage/internal/sqlstore/updated_at_lint_test.go
|
Review response from Kiran's (@Kiran01bm) AI code review assessment agent (Amp / Claude Opus 4.5) Summary: All three non-blocking findings are addressed — the dollar-quote tag fix landed on this branch, the UTC session pin is raised as a standalone PR, and the PR body wording is corrected; the fencing note will be answered on #1011.
Verified-correct section (ON CONFLICT targets, rebind ordinal alignment, Source: #1010 review 4919081588, posted by morgo (automated review on Morgan's behalf). |
…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
Adds the PostgreSQL nucleus to the shared
sqlstorecore: aPostgresDialectimplementing the binder, SQL-rendering, and identity seams that the dialect-parameterized store already consumes, plus the first cross-dialect parity lanes proving the shared SQL runs unmodified on PostgreSQL.Stacked on the SQL-portability and joined-DML seam PRs (#1007, #1009); base branch is the joined-DML branch.
What
PostgresDialect.Rebindrewrites store-native?placeholders to$nordinals, leaving?untouched inside single-quoted strings (including''escapes), double-quoted identifiers, dollar-quoted bodies ($$…$$,$tag$…$tag$— tags follow identifier rules, so a digit-leading$1stays a parameter),--line comments, and nested/* */block comments.EXCLUDED.colupserts viaON CONFLICT … DO UPDATE,now()timestamps (microsecond precision is native), and interval arithmetic (now() - ? * interval '1 second').InsertIfAbsentviaON CONFLICT (cols) DO NOTHING(conflict reports zero affected rows, same contract asINSERT IGNORE).JSONBooleanIsTrueviajsonb_extract_path(...) IS NOT DISTINCT FROM 'true'::jsonb— null-safe like MySQL's<=>, so missing paths, JSON null, and SQL NULL yield false rather than NULL.IndexHintrenders empty — PostgreSQL has no index-hint syntax.JoinedUpdateviaUPDATE … SET … FROM … WHERE (join) AND (predicate); SET assignments stay textually ahead of the predicate so?→$nordinal rebinding preserves the MySQL rendering's argument order.RETURNING id—InsertIDscans the generated key;InsertGuardedIDmaps the no-row case of a guardedINSERT … SELECTtoinserted=falseinstead of an error.NewWithDependenciesnow injects the configured identity/dialect into the task, apply-log, plan-comment, and settings stores, which previously pinnedMySQLDialect{}. MySQL callers passMySQLDialect{}for every seam; the one MySQL-visible change is thatsettings.Setnow renewsupdated_aton same-value writes, so the column tracks the last write rather than the last value change — deliberate, documented in code, and covered by tests.storagetest.TestSettingssuite and an apply-logs round-trip run against a real PostgreSQL container, plus pgx stdlib value-contract pins (boolean,jsonb[]byte,timestamptzround-trip precision,RowsAffectedmatched-rows semantics).Why
The store core is dialect-parameterized but until now only had a MySQL implementation, so the seams were exercised on one side only. This nucleus makes the second dialect real and lets each remaining store family be brought onto PostgreSQL in small, individually verifiable slices.
Deliberately out of scope (follow-up slices): joined DML for the operation/task/apply stores, growing the PostgreSQL parity lanes to the remaining store families, and the public PostgreSQL store constructor.