Skip to content

Commit 3c5f3c5

Browse files
claude[bot]claude
andauthored
fix(cli): a generated migration carries the field-level unique index driver-sql creates (#17208)
* fix(cli): a generated migration carries the field-level unique index the driver creates Both migration formats emitted the table and none of the object's declared uniqueness. Measured on live PostgreSQL 16.13, one object through all three producers, pg_indexes per schema: driver probe_pkey, uniq_probe_keyed_unique sql gen probe_pkey ts gen probe_pkey Two rows with the same value in a `unique: true` field were refused by the platform's table and accepted by both generated ones, with nothing reporting it. The key set was already computed here for #16091's column widths; only the index it implies was missing. The sql format emits an inline `CONSTRAINT <name> UNIQUE (...)` — what knex's `table.unique(columns, { indexName })` compiles to on PostgreSQL, so both pg_indexes and pg_constraint agree with the driver — and the ts format emits that knex call itself. Names come from a transcription of driver-sql's `buildIndexName` (#5726 forbids a static driver import from a CLI production module), pinned against the driver's own export. Two shapes stay unemitted and are now NAMED in the generated file rather than dropped: the ADR-0120 D3 organization-scoped composite, whose COALESCE key part knex's schema builder cannot express, and object-level `indexes[]`, which a second normalizer reads with different token semantics. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 * chore(changeset): patch @objectstack/cli for the emitted unique index Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4261fbc commit 3c5f3c5

4 files changed

Lines changed: 824 additions & 7 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): `os generate migration` emits the field-level unique index the driver creates (#16317)
6+
7+
## What was wrong
8+
9+
Both migration formats emitted the table and none of the object's declared
10+
uniqueness. Measured on live PostgreSQL 16.13 — one object driven through all
11+
three producers into three schemas, `pg_indexes` read back per schema:
12+
13+
```ts
14+
{ name: 'probe', fields: { keyed_unique: { type: 'text', unique: true, maxLength: 100 } } }
15+
```
16+
17+
| producer | before | after |
18+
|:--|:--|:--|
19+
| `driver-sql` via `initObjects` | `probe_pkey`, `uniq_probe_keyed_unique` | unchanged |
20+
| `--format sql` | `probe_pkey` | `probe_pkey`, **`uniq_probe_keyed_unique`** |
21+
| `--format ts` | `probe_pkey` | `probe_pkey`, **`uniq_probe_keyed_unique`** |
22+
23+
Two rows with the same `keyed_unique` value were refused by the platform's table
24+
(`23505 ... violates unique constraint "uniq_probe_keyed_unique"`) and accepted
25+
by both generated ones, with nothing reporting it: a scaffold that creates the
26+
table for an object silently dropped a uniqueness guarantee the object declares.
27+
After the change the duplicate is refused by all three, each naming the same
28+
constraint.
29+
30+
The key set was not missing — it was already computed here to size the keyed
31+
text family's columns; only the index it implies was never emitted.
32+
33+
## What it does now
34+
35+
- **`--format sql`** emits an inline `CONSTRAINT "<name>" UNIQUE (<columns>)`.
36+
That is what knex's `table.unique(columns, { indexName })` — the driver's own
37+
call — compiles to on PostgreSQL, so a generated table and a platform-created
38+
one agree in `pg_constraint` as well as in `pg_indexes`; and it stays inside
39+
the statement's `IF NOT EXISTS`, which a following `ALTER TABLE ... ADD
40+
CONSTRAINT` has no spelling for.
41+
- **`--format ts`** emits that knex call itself, `indexName` included — which is
42+
what makes the driver recognise the constraint as already present on its first
43+
boot against a generated table, instead of adding a second one under its own
44+
name and then reporting the generated one as an orphan to drop.
45+
- Names come from a transcription of `driver-sql`'s `buildIndexName`, pinned
46+
against the driver's own export (a CLI production module may not statically
47+
value-import a driver package).
48+
49+
## What it deliberately still does not emit — and now says so
50+
51+
Both formats print a `NOT EMITTED:` line naming the index, its key parts and the
52+
reason, instead of dropping it silently:
53+
54+
- the **organization-scoped composite** (`unique: true` / `'organization'` on an
55+
object with an organization column), whose key part is
56+
`COALESCE(<organization column>, '__global__')`. Emitting the bare composite
57+
instead would be worse than emitting nothing: under SQL's NULL-distinct
58+
`UNIQUE` it constrains no row that has no organization, which on a
59+
single-tenant deployment is every row.
60+
- an index over a column no field materialises (a virtual `formula` field) —
61+
the same skip the driver performs, where the driver logs a warning.
62+
63+
Object-level `indexes[]` remains unemitted by both formats; it is normalized by
64+
a different driver-side rule and is not covered by this change.

0 commit comments

Comments
 (0)