feat(storage): add PostgreSQL storage-table schema definitions - #936
Conversation
Translates the 13 embedded MySQL bootstrap files to pkg/schema/postgres/ and exposes them as schema.PostgresFS. updated_at stamping becomes the application's responsibility on PostgreSQL (no ON UPDATE CURRENT_TIMESTAMP equivalent; no trigger installed). Index names gain a table prefix because PostgreSQL index names share one schema-wide namespace.
There was a problem hiding this comment.
Pull request overview
Adds a PostgreSQL dialect copy of SchemaBot’s embedded storage-table schema files and wires them into pkg/schema as an embedded FS, with tests to enforce cross-dialect parity and validate that the DDL executes against a real Postgres instance.
Changes:
- Added
pkg/schema/postgres/*.sqlstorage-table DDL translations and embedded them asschema.PostgresFS. - Added unit tests to enforce file-set parity, table/index naming rules, and to lint against MySQL-only syntax in Postgres files.
- Added an integration test that executes all Postgres DDL files in a Postgres testcontainer and diffs column names + nullability against the TiDB-parsed MySQL schema.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/schema/schema.go | Embeds Postgres schema files alongside MySQL via PostgresFS. |
| pkg/schema/postgres/applies.sql | Postgres DDL for applies storage table and indexes. |
| pkg/schema/postgres/apply_comments.sql | Postgres DDL for apply_comments storage table and indexes. |
| pkg/schema/postgres/apply_control_requests.sql | Postgres DDL for apply_control_requests storage table and indexes. |
| pkg/schema/postgres/apply_logs.sql | Postgres DDL for apply_logs storage table and indexes. |
| pkg/schema/postgres/apply_operations.sql | Postgres DDL for apply_operations storage table and indexes. |
| pkg/schema/postgres/apply_target_locks.sql | Postgres DDL for apply_target_locks storage table and uniqueness/index rules. |
| pkg/schema/postgres/checks.sql | Postgres DDL for checks storage table and indexes. |
| pkg/schema/postgres/locks.sql | Postgres DDL for locks storage table and indexes. |
| pkg/schema/postgres/plan_comments.sql | Postgres DDL for plan_comments storage table and indexes. |
| pkg/schema/postgres/plans.sql | Postgres DDL for plans storage table and indexes. |
| pkg/schema/postgres/settings.sql | Postgres DDL for settings storage table and indexes. |
| pkg/schema/postgres/tasks.sql | Postgres DDL for tasks storage table and indexes. |
| pkg/schema/postgres/webhook_events.sql | Postgres DDL for webhook_events storage table and indexes. |
| pkg/schema/postgres_test.go | Unit tests enforcing MySQL/Postgres schema file parity and Postgres naming/syntax rules. |
| pkg/schema/postgres_integration_test.go | Integration test executing Postgres DDL and comparing column sets/nullability to MySQL (TiDB-parsed). |
| go.mod | Adds dependencies for pgx stdlib driver and testcontainers Postgres module. |
| go.sum | Adds checksums for newly introduced Postgres-related dependencies. |
Suppressed comments (1)
pkg/schema/postgres_integration_test.go:98
- The rows close error is discarded. If the driver reports an error on Close (e.g., protocol/stream issues), it’s useful for the test to surface it instead of silently ignoring it.
defer func() { _ = rows.Close() }()
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ty tests Review findings: the drift net compared only column names and nullability, so a missing or de-uniquified postgres index — which the future PG store's ON CONFLICT upserts depend on — or a wrong type/width passed every check. Add a structural index parity test (table, ordered columns, uniqueness, both directions), extend the integration test to assert the declared type mapping and varchar widths, cross-check the index-lint regex against literal CREATE INDEX counts, pre-pull postgres:16 in CI, and fix doc drift.
|
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 Armand and performed by his agent. Reviewed at head 3d04ec7. Verdict: clean — no fix-before-merge findings. The translations are faithful, the parity tests fail closed, and the two dimensions the tests don't cover (primary keys and column defaults) I audited by hand across all 13 tables and found zero divergences. The core risk in a hand-translated schema is a silent semantic gap the structural tests don't reach. The tests cover a lot mechanically — file-set parity both directions, index shape parity both directions (with a parse-count cross-check so an index the regex can't read fails loudly instead of escaping the lint), and a real-server integration test that reads back
Verified solid: every forbidden-construct lint token is genuinely absent from the postgres files and the lint's policy bans (charset/collate) are documented with their rationale; the index-name rules (table prefix, schema-wide uniqueness, 63-char identifier limit) match PostgreSQL's actual namespace semantics; the type mapping is declared exactly once ( This review was generated by Claude Code (claude-fable-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Approving on Armand's behalf after the adversarial correctness review above (no fix-before-merge findings). This stamp was left by Claude Code (claude-fable-5).
Summary
Adds the PostgreSQL translation of SchemaBot's 13 embedded storage-table DDL files under
pkg/schema/postgres/, exposed asschema.PostgresFS. This is the schema-definition slice of the dialect-split storage bootstrap; the PostgreSQLEnsureSchemabootstrapper and advisory locker follow in a separate PR.What
pkg/schema/postgres/*.sql: one file per storage table, mirroringpkg/schema/mysql/table-for-table and column-for-column. Each file holds oneCREATE TABLEplus itsCREATE INDEXstatements.schema.PostgresFSembed alongsideMySQLFS.AUTO_INCREMENT,ON UPDATE CURRENT_TIMESTAMP,unsigned,tinyint,datetime, engine/charset/collation options), table-name/filename agreement, and index-name rules (table prefix, schema-wide uniqueness, 63-char limit).postgres:16testcontainer, then diffsinformation_schema.columnsagainst the TiDB-parsed MySQL files — column sets, nullability, the declared type mapping, and varchar widths must match exactly.testcontainers-go/modules/postgres(pinned to the repo's existing testcontainers v0.40.0) andpgx/v5stdlib driver (test-only for now; the PostgreSQL store will use it next).Why
The storage bootstrap dispatch already fails closed for non-MySQL dialects; this supplies the PostgreSQL schema definitions it will route to.
Why a separate DDL directory rather than sharing the MySQL files (or generating both from one source): the differences are grammatical, not just type names — backtick quoting,
AUTO_INCREMENT, inlineKEYclauses,ENGINE/CHARSETtable options, and unsigned integer types are all syntax errors in PostgreSQL, while index names move from a per-table to a schema-wide namespace. The MySQL files are also format-pinned:EnsureSchemadiffs them via Spirit's TiDB parser, which requires canonicalSHOW CREATE TABLEform, so they cannot drift toward anything dialect-neutral. And several translations are deliberate engineering choices (jsonboverjson, identity columns, application-sideupdated_at) that a mechanical transliteration layer would get wrong or bury in special cases. Behavioral equivalence between the two schemas is enforced where it belongs — the cross-dialectstoragetestparity suite (#935) will validate every store against the same interface contract once both land, and this PR's structural tests (file-set, index, column-set/nullability/type/width parity against the TiDB-parsed MySQL files) catch schema drift directly.Translation decisions worth review:
updated_at: application-side stamping, no trigger. MySQL'sON UPDATE CURRENT_TIMESTAMPhas no PostgreSQL equivalent. Rather than installing per-table triggers in the bootstrap, the PostgreSQL store will stampupdated_atin its UPDATE statements, and the cross-dialect parity suite enforces the behavior. Columns keepDEFAULT CURRENT_TIMESTAMPfor inserts.idx_applies_repo_pr, notidx_repo_pr): MySQL index names are per-table, PostgreSQL's share one schema-wide namespace, so the MySQL names (reused across tables) would collide. A few names are also normalized where the MySQL name embedded its table (idx_apply_control_request_status→idx_apply_control_requests_status); the index parity test pins table/columns/uniqueness so renames can't hide a structural change.GENERATED BY DEFAULT AS IDENTITY, allowing explicit ids likeAUTO_INCREMENT),jsonbforjson,booleanfortinyint(1)(the Go structs arebool),bigintforint unsigned(PostgreSQL has no unsigned types;integerwould halve the value range),timestampfordatetime.datetimeis whole-second (except the one deliberatedatetime(6)lease-expiry column); bare PGtimestampis microsecond everywhere. This is deliberate — second-truncating withtimestamp(0)would replicate a MySQL storage artifact rather than a requirement, and the store layer already treats time precision as dialect-specific. Consequence to keep in mind: writtentime.Timevalues round-trip second-truncated on MySQL but exact on PostgreSQL, so cross-dialect behavioral tests must not assert exact stored-time equality across dialects.utf8mb4_0900_ai_ciis case- and accent-insensitive. Every varchar comparison, and every varchar unique index, dedups case-insensitively on MySQL but byte-wise on PostgreSQL. Nothing in the store layer normalizes case today, so this stands as an open decision for the PostgreSQL store PR:citext,LOWER()expression indexes, or a documented app-side canonicalization guarantee (e.g. for repository names, which GitHub treats case-insensitively but case-preservingly).AUTO_INCREMENT, an explicit-id backfill mustsetvalthe sequence afterwards or later default inserts collide. No code inserts explicit ids today; noting it for the store/backfill PR.Merge coordination: the file-set and index parity tests intentionally turn
mainred if a MySQL table or index lands without a postgres counterpart. Two open PRs interact: #867 addsmysql/check_refresh_requests.sql(and an index onchecks), #648 changesapplies/apply_operationsindexes. Whichever merges second must add the postgres counterpart — that's the tests doing their job.