Skip to content

feat(ddl): PostgreSQL EXCLUDE constraints with range-overlap support - #160

Merged
productdevbook merged 1 commit into
mainfrom
feat/exclude-constraints
May 19, 2026
Merged

feat(ddl): PostgreSQL EXCLUDE constraints with range-overlap support#160
productdevbook merged 1 commit into
mainfrom
feat/exclude-constraints

Conversation

@productdevbook

Copy link
Copy Markdown
Owner

Summary

  • PG EXCLUDE table-level constraint — a generalisation of UNIQUE where each element pairs a column/expression with a commutative SQL operator (room WITH =, during WITH &&). The flagship use case is range-overlap exclusion for booking systems, replacing race-condition-prone application code with a single DB-enforced invariant.
  • Supports the optional WHERE predicate (partial exclude — "at most one active row per priority") and the USING <method> clause (defaults to gist).
  • PG-only via the new EXCLUDE_CONSTRAINTS feature flag; MySQL / SQLite / MSSQL throw UnsupportedDialectFeatureError at print time.

Files

  • src/ast/ddl-nodes.ts — new ExcludeConstraintNode + ExcludeElement, added to the TableConstraintNode union.
  • src/schema/table.ts — new ExcludeDef + ExcludeElementDef types, excludes field on TableConstraints, normalizeExcludeDef helper.
  • src/migrate/diff.tsmaterializeExclude lowers ExcludeDef → AST; signConstraint folds method + elements + WHERE into the signature so any body change surfaces as drop+add.
  • src/printer/ddl.ts — emits EXCLUDE [USING <method>] (<expr> WITH <op>, …) [WHERE (<pred>)] on PG; refuses on every non-PG dialect via assertFeature("EXCLUDE_CONSTRAINTS").
  • src/dialect/features.ts — new EXCLUDE_CONSTRAINTS feature (PG only).
  • src/utils/security.ts — new validateOperator helper (1-4 ASCII punctuation char whitelist) — operator tokens are spliced verbatim into emitted DDL, so they get the same validation as function names and data types.
  • src/schema/index.ts — re-exports ExcludeDef / ExcludeElementDef.
  • test/migrate/exclude-constraints.test.ts — 18 new tests: schema-DSL preservation, diff materialization, drop+add on method/elements/WHERE change, four dialect-refusal cases, security validation of unsafe operators/method names, and a PGlite roundtrip that proves INSERT of an overlapping range fails with the constraint violation.
  • docs/recipes.md — new "PostgreSQL EXCLUDE constraints" section with the booking-system pattern, partial-exclude variant, and dialect-support matrix.

Test plan

  • pnpm fmt && pnpm lint && pnpm typecheck && pnpm vitest run all green
  • 18 new tests pass (17 unit + 1 PGlite roundtrip)
  • Total: 2215 passed, 58 skipped, 0 failed
  • Rebased onto origin/main (e4f6a4c — Agent B's dateAdd / dateSub landed clean)
  • No new runtime deps
  • No audit*-regressions.test.ts modifications

Tradeoffs

  • The PGlite roundtrip uses a single-element exclude (during WITH &&) rather than the composite (room WITH =, during WITH &&) because PGlite doesn't ship the btree_gist extension that the composite form needs for the equality element. The composite form is still covered end-to-end via the DDL printer test (which asserts the exact emitted SQL); the integration test then proves the constraint is actually enforced at INSERT time.
  • Unnamed constraint drops follow the existing diff-engine convention (skipped — most dialects require a name to drop). Naming is recommended for any constraint you might want to migrate later.

🤖 Generated with Claude Code

Generalises UNIQUE — each element pairs a column/expression with a
commutative operator (e.g. `room WITH =, during WITH &&`). Refused on
MySQL / SQLite / MSSQL via the EXCLUDE_CONSTRAINTS feature flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@productdevbook
productdevbook merged commit 4080aad into main May 19, 2026
1 check passed
@productdevbook
productdevbook deleted the feat/exclude-constraints branch May 19, 2026 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant