Skip to content

feat(migrations): support multi-column unique constraints (ZWEI-2850)#488

Merged
nicola-smartive merged 2 commits into
mainfrom
feat/ZWEI-2850-composite-unique-indices
Jun 11, 2026
Merged

feat(migrations): support multi-column unique constraints (ZWEI-2850)#488
nicola-smartive merged 2 commits into
mainfrom
feat/ZWEI-2850-composite-unique-indices

Conversation

@smartive-nicolai

Copy link
Copy Markdown
Contributor

Adds a new unique constraint kind to the entity model DSL for declaring composite (multi-column) unique indices, which previously had to be hand-written as raw-SQL knex migrations.

Why

The model DSL already supports single-column uniqueness via the field-level unique: true flag, but composite natural keys (join-table FK pairs, (parentId, name) combos, …) had no declarative form — consumers wrote raw CREATE UNIQUE INDEX migrations by hand. This adds first-class support.

Driven by ZWEI-2850 (zwei-wealth-platform wants ~20 composite unique indices declared in-model).

What

New constraint kind:

constraints: [
  {
    kind: 'unique',
    name: 'customer_relation',
    fields: ['customerId', 'relationId'], // DB column names, like `exclude` elements
    where: '"deleted" = false',           // optional partial predicate
  },
]
  • Emitted as a CREATE UNIQUE INDEX (not a UNIQUE constraint) so it can be partial via where — essential for soft-deletable entities, where a soft-deleted row should no longer block re-using the natural key. (Single-column unique: true is unchanged and remains the right tool for one column.)
  • The migration generator now reflects existing gm-managed unique indexes from pg_index, deliberately excluding any index backing a constraint (primary keys and field-level unique: true, which knex emits as ADD CONSTRAINT … UNIQUE) — so those stay managed by their own code paths and are never touched here.
  • add / drop / recreate-on-change is detected on regenerate, mirroring the existing check / exclude / constraint_trigger diffing. Diff compares the ordered column list + normalized where predicate against pg_get_indexdef output.
  • Validation mirrors check/exclude: unknown column names and empty fields are rejected, enforced both at Models construction and at migration generation.

Files

  • src/models/model-definitions.ts — new unique kind on ConstraintDefinition + UniqueConstraintDefinition export
  • src/models/utils.tsvalidateUniqueConstraint
  • src/models/models.ts — validate at construction
  • src/migrations/generate.ts — reflect existing unique indexes, emit/diff on create + alter paths, helper methods
  • docs/docs/2-models.md — documents the new kind
  • tests/unit/migration-constraints.spec.ts — 7 new unit tests

Verification

  • tsc clean
  • npm run lint clean
  • full unit suite 96/96 pass (incl. the 7 new)

🤖 Generated with Claude Code

nicola-smartive and others added 2 commits June 11, 2026 14:48
Adds a new `unique` constraint kind to the entity model DSL for declaring
composite (multi-column) unique indices, which previously had to be written
as hand-rolled raw-SQL knex migrations.

It is emitted as a `CREATE UNIQUE INDEX` (not a `UNIQUE` constraint) so it can
be partial via an optional `where` predicate — essential for soft-deletable
entities, where `'"deleted" = false'` keeps a soft-deleted row from blocking
re-use of the natural key. For single columns the field-level `unique: true`
flag remains the right tool.

- model DSL: `{ kind: 'unique'; name; fields: string[]; where? }`
- migration generator reflects existing gm-managed unique *indexes* from
  pg_index (excluding constraint-backed ones, so field-level `unique` and
  primary keys are untouched) and add/drop/recreates on diff
- validation mirrors check/exclude (unknown column + empty fields rejected),
  enforced both at Models construction and migration generation
- docs + 7 unit tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves conflicts with #487 (declarative regular trigger support): the new
`trigger` constraint kind and this branch's `unique` kind now coexist in the
ConstraintDefinition union, the migration generator's reflection queries +
create/alter loops, and the test suite. Resolution tree verified: tsc + lint
clean, 102/102 unit tests pass.
@nicola-smartive nicola-smartive merged commit ec295dd into main Jun 11, 2026
12 checks passed
@nicola-smartive nicola-smartive deleted the feat/ZWEI-2850-composite-unique-indices branch June 11, 2026 20:32
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 27.3.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant