Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/generated-migration-character-column-widths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@objectstack/cli": minor
---

`os generate migration` now emits the character column `driver-sql` actually creates, in both the TypeScript and the SQL format.

A `text` field took `VARCHAR(255)` from both generators while the platform creates an unbounded `text` column for it, so a 300-character value the platform stores was refused by every generated table with `value too long for type character varying(255)`. Enumerating the whole character-column family found the same disagreement in eight more places: `url` and `phone` and `color` carried widths the generators invented (2048, 50 and 7 against the platform's 255), and neither generator read a field's declared `maxLength` at all, so a `maxLength: 400` email was `varchar(400)` on the platform and `varchar(255)` in the migration generated for it.

All of them now follow the platform's own three answers: the text family is unbounded unless the object KEYS the column — a field declared `unique`, or one an object-level `indexes[]` entry lists, takes `varchar(maxLength)` up to the 768-character key-part ceiling, exactly as the platform builds it, and stays unbounded above that ceiling or with no declared bound, where the declared bound is enforced at the write seam instead — the string family takes its declared `maxLength` verbatim in both directions, and TEXT rather than a clamp when it exceeds what a `varchar` can express, and the remaining string-valued types keep the default width and ignore a declaration, because their stored value is an option code or another row's id rather than the declared string.

The keyed half was measured after the rest: `{ type: 'text', unique: true, maxLength: 100 }` built `varchar(100)` on the platform and `text` in both generated tables, so a 300-character value the platform REFUSES was accepted by every generated table — the same disagreement as the headline row, pointing the other way.

This scopes to PostgreSQL, which is the only dialect `os generate migration --format sql` claims.
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,13 @@ describe('the builtin id column both migration generators emit (#15040)', () =>
expect(sql.indexOf('"id"')).toBeLessThan(sql.indexOf('"title"'));
const ts = generateMigrationTs(CONFIG as Record<string, unknown>);
expect(ts).toContain("await db.schema.createTable('account'");
expect(ts.indexOf("table.string('id')")).toBeLessThan(ts.indexOf("table.string('title')"));
// #16091 — matched on the field NAME rather than on its column method. The
// assertion is about ORDER (the primary key comes first), and a reader keyed
// to `table.string` silently became `indexOf(…) === -1` the moment `title`,
// a `text` field, moved to `table.text` — which reads as a passing
// "less than" only until you notice what it is less than.
expect(ts.indexOf("table.string('id')")).toBeLessThan(ts.indexOf("('title')"));
expect(ts.indexOf("('title')"), 'the title column vanished from the output').toBeGreaterThan(0);
// Each generator carries exactly ONE hardcoded id line — the shape that let
// these two disagree with the driver in the first place, and the reason a
// fix to one of them can silently leave the other behind. Counted over the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,18 @@ describe('#14828 — the SQL answers are the platform’s, not this file’s inv
expect(tsInterfaceType('number')).toBe('number');
// The driver's own answer for the headline member, read where it lives.
expect(createColumnArm('autonumber')).toContain('table.string(name)');
expect(sqlColumn('autonumber')).toBe(sqlColumn('text'));
// #16091 — compared against `lookup`, not against `text`. Both were
// `VARCHAR(255)` when this line was written, which made `text` a usable
// stand-in for "the driver's default string column"; it is not one any
// more. `createColumn` gives `text` its text-family arm (an unbounded TEXT
// for every unkeyed column) and gives `lookup` the same bare
// `table.string(name)` it gives `autonumber` — asserted here, from the
// driver, so the comparator cannot silently become a different question again.
expect(createColumnArm('lookup')).toContain('table.string(name)');
expect(sqlColumn('autonumber')).toBe(sqlColumn('lookup'));
// Anti-vacuity: the comparator is a real, DIFFERENT answer from the
// text family's, so this equality is a measurement rather than a tautology.
expect(sqlColumn('autonumber')).not.toBe(sqlColumn('text'));
expect(tsColumn('autonumber')).toBe("table.string('f_autonumber')");
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,14 @@ describe('#14829 — `multiple: true` is one answer across all three surfaces',
// control cannot be satisfied by one column shape for everything either.
expect(sqlColumn('single_lookup')).toBe('VARCHAR(255)');
expect(tsColumn('single_lookup')).toBe("table.string('single_lookup')");
expect(sqlColumn('single_text')).toBe('VARCHAR(255)');
expect(tsColumn('single_text')).toBe("table.string('single_text')");
// #16091 — `text` is an unbounded TEXT column now, which is what
// `createColumn`'s text-family arm builds for every unkeyed column. The
// control is unweakened by that for exactly the reason the `lookup` note
// above gives: what it discriminates is scalar-vs-JSON, and TEXT is scalar.
expect(sqlColumn('single_text')).toBe('TEXT');
expect(tsColumn('single_text')).toBe("table.text('single_text')");
// …and it still discriminates: the scalar answer is not the JSON one.
expect(sqlColumn('single_text')).not.toBe(sqlColumn('multi_text'));
expect(sqlColumn('single_file')).toBe('VARCHAR(2048)');
expect(tsInterfaceType('single_lookup')).toBe('string');
});
Expand Down Expand Up @@ -285,8 +291,8 @@ describe('#14829 — `multiple: true` is one answer across all three surfaces',
// this card does NOT touch is present in both outputs. Without it, "does
// not contain" would pass on an empty string.
expect(other).toContain('CREATE TABLE IF NOT EXISTS "probe" (');
expect(other).toContain('"t" VARCHAR(255)');
expect(otherTs).toContain("table.string('t')");
expect(other).toContain('"t" TEXT');
expect(otherTs).toContain("table.text('t')");

// A RENDERED string (prefix + counter + suffix), never an integer sequence.
expect(other).toContain('"a" VARCHAR(255)');
Expand Down
Loading
Loading