Skip to content

Commit 08706f0

Browse files
os-trumpclaude
andauthored
fix(cli): os generate migration emits the columns the platform actually creates (#15076)
* fix(cli): generate migration emits the columns the platform creates (#14828) Five field-type vocabulary entries in packages/cli/src/commands/generate.ts keyed on real FieldType members and described DDL driver-sql does not create. Each value is now read from the driver, the emitter that creates the real columns: autonumber SERIAL -> VARCHAR(255) (a RENDERED string, table.string) multiselect TEXT -> JSONB (MULTI_OPTION_TYPES seeds JSON) vector VECTOR -> JSONB (STRUCTURED_JSON_TYPES; not portable) formula TEXT -> no column (virtual: createColumn returns) lookup/md uuid, 36 -> table.string, VARCHAR(255) (a platform id is 26 chars; Postgres 22P02 on uuid) user/tree move with lookup/master_detail so REFERENCE_VALUE_TYPES keeps one answer: a reference column holds the target's id, and the driver emits that as table.string('id').primary(). The rule is derived, not retyped. generate-field-type-vocabulary.pin.test.ts sweeps the spec's ADR-0104 D1 value classes and source-reads createColumn's own switch; #14829's scope fence in generate-multiple-json-column.pin.test.ts is discharged in place rather than deleted or routed around. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza * fix(cli): select the virtual answer by own-property presence, not nullish-coalescing (#14828) Three defects the new pin caught on its first run, all real: 1. `FIELD_TYPE_SQL_MAP[fieldType] ?? 'TEXT'` still emitted `"f" TEXT` for a formula field — `??` falls through on `null`, which is exactly the value that means "virtual". Own-property presence is the only spelling that distinguishes "absent" (the unvalidated authoring door) from "no column", and `hasOwnProperty` rather than `in` because `in` answers true for `toString` and every other inherited key. 2. The pin's createColumn arm extractor stopped ON the terminator, dropping the trailing same-line comment the driver states the arm's reason in. 3. The within-file rule "a string on the record takes a character column" was too strong: `date` / `datetime` / `time` are strings on the record and take temporal columns correctly. Narrowed to what `autonumber` actually did — a rendered string in a column that only accepts numbers — with the predicate itself controlled against SERIAL and a genuinely numeric member. 44/44 green across both pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza * tooling(cli): declare field.zod.ts as a cross-package test input (#14828) #14828 gave generate-field-type-vocabulary.pin.test.ts its first cross-package READ — driver-sql's sql-driver.ts and schema-drift.ts, the authority on which column each field type gets. That is what brings a file into check:cross-package-test-inputs' scan at all, and the flat literal collector then took the one quoted path already in its header prose: the packages/spec/src/data/field.zod.ts that #13871 cites as the file it ran `git log -S` over. Declared rather than reworded, exactly as translation.zod.ts was for the same shape, and honestly rather than as a shrug: field.zod.ts DEFINES the FieldType enum this pin asserts totality over, so a member added there is precisely the change that must re-run this suite. Control run: restoring only the pin file to its pre-card bytes turns the gate green again (exit 0, "25 package(s) read outside themselves, all declared"), which is what identifies this card's read as the cause rather than a pre-existing miss. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d261cef commit 08706f0

6 files changed

Lines changed: 630 additions & 62 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): `os generate migration` now emits the columns the platform actually creates for `autonumber`, `multiselect`, `vector`, `formula` and the reference types
6+
7+
Five entries in the CLI's field-type vocabularies keyed on real `FieldType`
8+
members and described DDL the platform does not create. #13871 removed entries
9+
naming types that do not exist and #14657 added entries for real members that
10+
had none, deliberately leaving every pre-existing entry alone; this is the third
11+
direction, and every value below is now read from
12+
`packages/drivers/driver-sql/src/sql-driver.ts`, the emitter that creates the
13+
real columns.
14+
15+
- `autonumber` was `SERIAL`. The runtime issues a **rendered string** — prefix,
16+
counter, suffix — and `createColumn` gives it `table.string(name)`. A SERIAL
17+
is an integer column with a sequence attached, so Postgres answered
18+
`22P02 invalid input syntax for type integer` for `INV-0001`. The file already
19+
contradicted itself here: `os generate types` has always called this member a
20+
`string`. Now `VARCHAR(255)`.
21+
- `multiselect` was `TEXT`. `MULTI_OPTION_TYPES` seeds the driver's
22+
`JSON_COLUMN_TYPES`, so the runtime writes a JSON array. A text column is the
23+
silently corrupting shape — `schema-drift.ts` gates its own multi-value
24+
finding on exactly `char|text` because "the textual family is the one that
25+
says yes and corrupts" — and the array landed as the literal `'["a","b"]'`,
26+
read back as one opaque string. Now `JSONB`.
27+
- `vector` was `VECTOR`. `vector` is in `STRUCTURED_JSON_TYPES`, hence a JSON
28+
column. `VECTOR` also needs the pgvector extension and does not exist on MySQL
29+
or SQLite, so the generated `CREATE TABLE` failed outright off Postgres. Now
30+
`JSONB`.
31+
- `lookup` and `master_detail` took `table.uuid` in the TypeScript migration.
32+
This was the one hard failure: a platform id is 26 characters, and Postgres
33+
refuses one in a `uuid` column with `22P02` on the first insert. Both now take
34+
the driver's own answer for a reference column, `table.string` /
35+
`VARCHAR(255)` — and `user` / `tree` move with them, because a reference
36+
column holds the target's `id`, which the driver emits as
37+
`table.string('id').primary()`. One class, one answer.
38+
- `formula` was given a column. It is **virtual**: `createColumn` answers
39+
`case 'formula': return;` and `schema-drift.ts`'s `fieldHasColumn` answers
40+
false for it, so the generated migration created a column the runtime never
41+
writes to. Both migration generators now emit nothing for it, carried as the
42+
vocabulary's own `null` answer so the two cannot disagree about which fields
43+
materialise. A `formula` field is still declared on the generated record type
44+
— it is readable, just not stored.
45+
46+
A generated migration is scaffolding you run once and then own, so this changes
47+
what the **next** generation emits and reaches into no database that has already
48+
migrated. A project that has run an older generated migration keeps its columns;
49+
where they differ from the platform's, `os migrate plan` reports the drift.
50+
51+
The values are pinned rather than restated: `generate-field-type-vocabulary.pin.test.ts`
52+
sweeps the spec's ADR-0104 D1 value classes and reads the driver's own switch,
53+
so a fourth hand-carried table of right answers cannot arrive. The file family
54+
(`file` / `image` / `avatar` / `video` / `audio`) is deliberately unchanged and
55+
recorded as an open divergence rather than corrected — the driver is pre-D3
56+
there and the generator is post-D3, which is a decision, not a wrong value.

0 commit comments

Comments
 (0)