Skip to content

Commit 000fd05

Browse files
os-litantclaude
andauthored
fix(cli): generated migrations emit the character column driver-sql creates (#16298)
* fix(cli): generated migrations emit the character column driver-sql creates (#16091) Both migration generators capped a `text` field at VARCHAR(255) while `driver-sql` 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)`. #15521's ruling names this card and settles its direction -- the generator follows the driver, as #15040 already did for the `id` column in this same file. Driven on a private PostgreSQL 16.13 cluster, all three producers run from one object and their columns read back out of `information_schema.columns`. The sweep found nine divergent columns of 26 probed, not one: text driver text gen varchar(255) both formats text+max driver text gen varchar(255) maxLength must NOT size it email+max driver varchar(400) gen varchar(255) maxLength was never read url driver varchar(255) sql varchar(2048) invented width phone driver varchar(255) sql varchar(50) invented width color driver varchar(255) sql varchar(7) invented width All of them now follow `createColumn`'s three arms. The text family is unbounded, because that arm branches on KEYED and a generated migration emits no index; its declared bound is enforced at the write seam, not by the column. The string family takes `declaredVarcharLength`'s answer -- the declaration verbatim in both directions, knex's 255 without one, and TEXT above the varchar ceiling rather than a clamp to it. The catch-all keeps the default width and ignores a declaration, because its stored value is an option code or another row's id rather than the declared string. Driven again afterwards: 0 of 26 columns diverge, and the 300-character write is accepted in all three tables exactly where the platform accepts it and refused in all three exactly where the platform refuses it. `generate-string-family-width.pin.test.ts` asserts that agreement against the driver's own source -- arm membership read from `createColumn`'s case labels, widths read from its own constants -- so a driver that moves fails there instead of leaving the generators quietly wrong. Three existing pin files move with it: two used `text`'s old VARCHAR(255) as a stand-in for the driver's default string column, and one asserted column ordering by searching for a `table.string` call that is now a `table.text` call. Scope is PostgreSQL, the only dialect `--format sql` claims (#15521). The FILE_REFERENCE_TYPES divergence stays recorded and unresolved (#15041). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * chore(changeset): grade `@objectstack/cli` minor, the level a declared clause-② requires `Check Changeset`'s LEVEL AXIS (#16055) refuses a PR that declares clause-② YES while grading a package whose `packages/*/src/**` it moves as `patch`. The rule it mechanizes is the maintainer's 2026-09-04 ruling (decision batch #35, on #15294), written out under "WHICH LEVEL" in that step: a purely additive widening of a published package's public surface takes AT LEAST `minor`, and the commit type may raise a bump but never lower it below what the act requires. This branch declares clause-② `yes` and moves `packages/cli/src/**`, so the level and the declaration contradicted each other. Only the level moves here -- the generators, the pins and the measurements are untouched. ⚠️ The axis is invisible to the plain `--base origin/main` form of the gate, which reports `LEVEL AXIS: NOT MEASURED` and is neither a pass nor a failure. It is judged only from a `pull_request` event payload, off the `needs:contract-review` carrier or a machine-spelled `Clause-②:` line, so `--event` is the only form that can confirm this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * fix(cli): size a KEYED text-family column from its declaration, as the driver does CORRECTING THE RECORD. This branch's first commit, the new pin's docblock and three comments in `generate.ts` all said: "the text family branches on KEYED, and a generated migration emits no index, so no generated column is ever keyed." That sentence describes this GENERATOR'S OUTPUT. `createColumn` reads the object's INPUT. Its `keyed` argument is `indexedKeyColumns(...).get(name)`, and `indexedKeyColumns` composes `uniqueIndexesFromFields` -- which keys a column on `field.unique`, a key every `FieldSchema` carries -- with the object's declared `indexes[]`. Both are DECLARATIONS, both are in the config these generators already read, and neither has anything to do with what a migration emits. The generator could have read `unique`; it simply did not. So a keyed text-family column IS sized from its declaration, at `keyableTextLength`'s width: the declared `maxLength` verbatim up to MAX_KEYABLE_VARCHAR_CHARS (768, the widest one utf8mb4 key part holds), and unbounded above that ceiling or with no usable declaration. Driven on live PostgreSQL 16.13 against the pre-change tree, one 300-character write into `{ type: 'text', unique: true, maxLength: 100 }`: driver varchar(100) REFUSED -- 22001 character varying(100) sql gen text ACCEPTED -- read back at length 300 ts gen text ACCEPTED -- read back at length 300 The wide direction, which this branch's own body calls the quieter of the two, inside the family it claimed to have closed. Re-driven after the change, all three producers REFUSE it, and 0 of 32 keyed character columns diverge. WHAT MOVES * `generate.ts` gains `indexKeyColumns`, a mirror of the driver's own composition -- field-level `unique` at all three spellings, object-level `indexes[]` unique or not, and the ADR-0120 D3 tenant key part, whose resolution (`tenancy.enabled`, `tenancy.tenantField`, an `organization_id` column) is computable from the object alone and so is mirrored rather than skipped. It also gains `keyableTextChars` and the transcribed 768 ceiling, kept deliberately separate from `declaredVarchar`: the two answer different questions of the same key. * The false sentence is corrected in all four places it reached. * The new pin gains the keyed arm: the driver-source chain at every link, the arm membership held equal to `createColumn`'s case labels, the width sweep at both outcomes, the three unique spellings against the words the spec rejects, the object-level index half, and the tenant-column half -- each of the last two confirmed against the live cluster before pinning. TWO RIDERS FROM THE SAME REVIEW * The pin's catch-all case skipped any member whose plain answer had already drifted, so it measured that the catch-all takes the driver's default width only where that already held. Mutating `radio` or `secret` to 'TEXT' passed all 61 tests across all four pin files. The character half of the catch-all is now DERIVED from the three spec classes `driver-sql` seeds `JSON_COLUMN_TYPES` from -- imported, never listed -- and `VARCHAR(255)` is asserted on the rest. Both mutations now redden. * A comment gave a false reason for transcribing `MAX_VARCHAR_CHARS`: "`packages/cli` does not depend on the driver at runtime". It does -- `@objectstack/driver-sql` is in this package's `dependencies` at `workspace:^`. The transcription is still necessary, for two other reasons: the constant is `protected static`, and #5726 forbids a CLI production module any static value import of a driver package. The reason moves; the transcription does not. The changeset stays `minor` and states the keyed half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * test(cli): give the two mirrored driver bodies a driver-side oracle `generate.ts` mirrors four things `driver-sql` owns. Two of them were already falsifiable from the driver: `MAX_KEYABLE_VARCHAR_CHARS` is compared against the constant's own declaration and `TEXT_FAMILY_TYPES` against `createColumn`'s own case labels, and a driver-side mutation of either reddens the pin. The other two mirror driver BODIES, which a source reader cannot see move — mutating `keyableTextLength` to clamp instead of answering null, and each of five mutations across `schema-drift`, `computeTenantField` and spec's `isUniqueDeclared`, left all 69 pins green. Both are now recomputed from `driver-sql` itself and compared: - the key set, from the driver's own exported `uniqueIndexesFromFields` and `normalizeDeclaredIndex` with the tenant column from a `SqlDriver` subclass that publishes `computeTenantField`, over a swept corpus of 1,224 objects (every combination of a field-level `unique` spelling, an `indexes[]` entry, a `tenancy` declaration and a column shape), against the key set read back out of what both generators emit; - both widths, from the driver's own `keyableTextLength` and `declaredVarcharLength` through the same subclass, over 37 declarations including the coerced and rejected spellings. A test file is not a CLI production module: #5726 governs `packages/cli/src/**` production sources, and the gate enforcing it excludes `*.test.ts` by construction. The package already declares `@objectstack/driver-sql` and the specifier is already in `KNOWN_UNALIASED_TEST_IMPORTS`, so neither the dependency graph nor that shrink-only ledger moves. The differential found one branch of `indexKeyColumns` disagreeing with the driver, and this fixes it. `normalizeDeclaredIndex` filters an entry's `nullSafeColumns` against its listed columns, but that filter narrows only `nullSafeColumns` — its `columns` stay the listed ones in every branch of the arm. Reading the filter as if it decided the KEY PARTS made `{ fields: ['f'], unique: 'organization', nullSafeColumns: ['zzz'] }` key `{organization_id, f}` here against the driver's `{f}`: a column bounded in a generated migration that the platform leaves unbounded. The condition is now the driver's own — a non-empty array, nothing more — and the comment claiming the mirrored branch kept this set from being a strict superset of the driver's is replaced, since that branch was the one making it exactly that. `isUniqueDeclared` and `isTenancyDisabled` are imported from `@objectstack/spec/data` rather than transcribed. Spec is not a driver package, so #5726 never reached them, and `isTenancyDisabled` is ADR-0066's single judgment for the registry, the engine and every driver. The transcriptions that remain now state their real warrant: `MAX_VARCHAR_CHARS`, `MAX_KEYABLE_VARCHAR_CHARS`, `keyableTextLength`, `declaredVarcharLength` and `computeTenantField` are `protected` and reach no exported surface, while `isOrganizationScopedUnique` is exported and is spelled here only because these generators are synchronous and #5726 leaves a production module `await import()` alone for a driver package. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * test(cli): ask the driver's own unique predicates, not their source text The two scope predicates `generate.ts` mirrors were pinned by reading `schema-drift.ts` for the exact line each is spelled on. That catches a rewording and nothing else: a driver whose vocabulary narrows while the line survives leaves the generators sizing a column the platform would not key, and the pin green. Both are exported, so the pin now ASKS them — `isUniqueScopeDeclared` over sixteen `unique` spellings against the width each produces in the emitted DDL, and `isOrganizationScopedUnique` over the same spellings against whether the tenant column is keyed with them. Measured by mutating the driver's `isUniqueScopeDeclared` to drop the bare-`true` and `'global'` spellings, rebuilding `driver-sql` and re-running: five pins go red, of which four are reachable only through the oracle. This is also the axis the `@objectstack/spec/data` import closes. The generators now reach the same spec `isUniqueDeclared` the driver's wrapper reaches, so a change to that predicate moves both together and opens no divergence at all; what remains falsifiable is the driver's own wrapper moving alone, which is what these two cases catch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * test(cli): make the oracle enter initObjects, not re-compose its leaves CORRECTING THE RECORD, first. Commit 11d0e8d's message states "a swept corpus of 1,224 objects" and "37 declarations". Both counts are wrong, and this queue composes the squash body from the branch's commit messages, so they would land in `main` as written. Counted mechanically by parsing the array literals and confirmed by generating the corpus: keyProbeCorpus() 6 uniques x 16 indexSets x 6 tenancies x 2 shapes = 1,152 WIDTH_DECLARATIONS 38 Four of those sixteen index shapes are the already-normalized ones, not three. Nothing in the suite caught either number: the only size assertion was `> 200`, which every wrong count satisfies. Both are now pinned as exact literals, so a corpus that grows without its stated size growing fails here rather than putting a false measurement into a permanent record. ASKING THE DRIVER'S LEAVES IS NOT ASKING THE DRIVER Round 2 transcribed the driver's answers, and mutating the driver left every pin green. Round 3 asked the driver's exported LEAVES -- `uniqueIndexesFromFields`, `normalizeDeclaredIndex`, `computeTenantField` -- and then RE-COMPOSED them in the test file, which left every layer between those leaves and the emitted column a second copy of the pin's own belief. It never called the driver's own `indexedKeyColumns`, nor `initObjects`' wiring of `tenantField` into it, nor `createColumn`'s dispatch on `keyed`. Measured driver-side at f3661ac, each mutation rebuilt into `dist`: indexedKeyColumns stops recording declared indexes 78 passed (78) initObjects passes tenantField: null into it 78 passed (78) against 764 and 276 of 1,152 objects respectively diverging between the real `initObjects` and the generators. Both of those are this card's own subject -- the driver changes what it keys and the generated column stays bounded where the platform's is unbounded -- and the instrument reported everything fine. The reddening of the pin as it now stands, under both mutations, is recorded in the PR body with its counts. WHAT MOVES The authority in the pin is now `SqlDriver.initObjects` on the in-memory better-sqlite3 driver the file already constructs, read back with `PRAGMA table_info`. That is computeAndRecordTenantField -> indexedKeyColumns -> createColumn -> knex -> an actual column, with nothing re-derived in the test. Two differentials run over it: * the whole 1,152-object corpus, comparing all 4,032 declared columns against both generators' emitted width; * every character TYPE the driver cases or catches -- membership read off `createColumn`'s own case labels and its catch-all derivation, 18 today -- at all 38 declarations, keyed and unkeyed, 1,368 probes. The leaf differential is KEPT underneath, because it localises a failure to one builder, and is now documented as NOT the authority. The width differentials against `keyableTextLength` / `declaredVarcharLength` are kept for the same reason: they say which method body moved, while the real chain also covers `createColumn`'s dispatch onto them. Each probe mints its own table name. `initObjects` takes the ALTER path on a name it has already seen and an ALTER cannot retype a column, so a shared name would report the first probe's answer for all 1,152. The driver's warnings are captured into the subclass rather than printed -- the corpus deliberately carries index shapes whose key parts name no materialized column, and the driver correctly says so 144 times on a green run, which is how a real warning stops being read. `logger` is the driver's own documented injection point; nothing about its behaviour changes and the messages stay available to a failure report. TWO SENTENCES THAT WERE STILL WRONG * The pin still said "`packages/cli` does not depend on the driver at runtime, so the ceiling is transcribed in generate.ts" -- verbatim the reason this branch already established as false, that `generate.ts` carries with a ban, and that the same test file contradicts 500 lines earlier. Replaced with the real reasons: `MAX_VARCHAR_CHARS` is `protected static` and reaches no exported surface, and #5726 leaves a CLI production module only `await import()`, which these synchronous generators cannot use. * `generate.ts`'s `isUniqueScopeDeclared` docblock restated a stale driver comment as present fact. Measured against the built spec, `isUniqueDeclared('organization')` is already `true`, so the disjunct is redundant today and both halves are spec's. The disjunct stays -- it is the driver's spelling and the mirror matches it character for character -- but it is no longer described as a scope spec does not accept. The changeset said the generators invented `2048 / 50 / 7`. Only the SQL format did; the TypeScript format emitted a bare `table.string(name)` for all three. Release-notes input, so it is corrected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * docs(cli): correct the reason the driver constants are transcribed CORRECTING THE RECORD. Three commit messages on this branch, and three sites in the code, give as their FIRST reason for transcribing the driver's constants that those members reach no exported surface. That is false, and this queue composes the squash body from the branch's commit messages, so the sentences below would land in `main` as written: * 9cc1a76 -- "The transcription is still necessary, for two other reasons: the constant is `protected static`, and #5726 forbids a CLI production module any static value import of a driver package." * 11d0e8d -- "`MAX_VARCHAR_CHARS`, `MAX_KEYABLE_VARCHAR_CHARS`, `keyableTextLength`, `declaredVarcharLength` and `computeTenantField` are `protected` and reach no exported surface". * 722880a -- "Replaced with the real reasons: `MAX_VARCHAR_CHARS` is `protected static` and reaches no exported surface, and #5726 leaves a CLI production module only `await import()`". `protected` is a COMPILE-TIME visibility modifier. It removes a member from neither the exported class nor the published types. Measured on this worktree's built `packages/drivers/driver-sql/dist`: index.d.ts:5593 protected static readonly MAX_VARCHAR_CHARS = 16383; index.d.ts:5536 protected static readonly MAX_KEYABLE_VARCHAR_CHARS = 768; index.d.ts:5625 protected declaredVarcharLength(field: any): number | null; index.d.ts:5626 protected keyableTextLength(field: any): number | null; index.d.ts:3501 protected computeTenantField(schema: ...); require('.../driver-sql/dist/index.js').SqlDriver.MAX_VARCHAR_CHARS -> 16383 hasOwnProperty.call(SqlDriver, 'MAX_VARCHAR_CHARS') -> true All five are on the exported `SqlDriver`. The pin test already depends on this: it reaches the driver's own `protected` judgments by subclassing, which it could not do if they were absent from the published types. THE REAL CONSTRAINT, AND IT IS A CHOICE #5726 forbids a CLI production module any static value import of an `@objectstack/driver-*` package -- `schema-migrate.lazy-driver-import.test.ts` scans every non-test `.ts` under `packages/cli/src` -- and what it leaves open is `await import()` at the point of use. These generators are SYNCHRONOUS, so they cannot take it. That is the whole reason, and it is a property of how this package is written rather than of the constants: make the generators async and the transcription can go. This is the THIRD round on the same claim. Round 3's review flagged it, round 4 retracted it in the PR body and left it standing at three sites in the code, which is what produced this round. The PR body is not what the next author reads; the comment beside the constant is. WHAT MOVES -- comments and docblocks only. No behaviour, no test, no pin, no count, no changeset: * `generate.ts`, `MAX_VARCHAR_CHARS`'s docblock: the "protected static, so not on the driver package's exported surface at all" bullet is gone. The reason is now #5726 plus the synchronous generators, and the retracted claim is kept as a banned one beside the "does not depend on the driver at runtime" ban that preceded it, so nobody restates it a fourth time. * `generate.ts`, `isOrganizationScopedUnique`'s docblock: it drew a contrast -- "Unlike {@link MAX_VARCHAR_CHARS}, this one IS on `driver-sql`'s exported surface" -- that the measurement above dissolves. Both reach the exported surface, and both are spelled here for the one reason. Its tail also named the LEAF differential as what makes the spelling safe; the authority since 722880a is `SqlDriver.initObjects` read back with `PRAGMA table_info`, with the leaf differential kept beneath it and explicitly not the authority. * `generate.ts`, `indexKeyColumns`: the same stale attribution -- "the differential ... which now recomputes this whole set from the driver's own exported builders" -- now names the real chain and marks the leaf as not the authority. * `generate-string-family-width.pin.test.ts`: the restatement 722880a put there is retracted in place, beside the earlier false reason that comment already bans. `MAX_KEYABLE_VARCHAR_CHARS`'s docblock inherits by reference -- "Transcribed and pinned for exactly the reasons {@link MAX_VARCHAR_CHARS} gives" -- so it is corrected by the block it cites and needed no edit. Verified: `pnpm --filter @objectstack/cli typecheck` exit 0; the four pin files `Test Files 4 passed (4)` / `Tests 81 passed (81)`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3890244 commit 000fd05

6 files changed

Lines changed: 2192 additions & 19 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
`os generate migration` now emits the character column `driver-sql` actually creates, in both the TypeScript and the SQL format.
6+
7+
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: the SQL format gave `url` and `phone` and `color` widths nothing on the platform has (2048, 50 and 7 against the platform's 255), and neither format 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.
8+
9+
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.
10+
11+
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.
12+
13+
This scopes to PostgreSQL, which is the only dialect `os generate migration --format sql` claims.

packages/cli/src/commands/generate-builtin-id-column.pin.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,13 @@ describe('the builtin id column both migration generators emit (#15040)', () =>
226226
expect(sql.indexOf('"id"')).toBeLessThan(sql.indexOf('"title"'));
227227
const ts = generateMigrationTs(CONFIG as Record<string, unknown>);
228228
expect(ts).toContain("await db.schema.createTable('account'");
229-
expect(ts.indexOf("table.string('id')")).toBeLessThan(ts.indexOf("table.string('title')"));
229+
// #16091 — matched on the field NAME rather than on its column method. The
230+
// assertion is about ORDER (the primary key comes first), and a reader keyed
231+
// to `table.string` silently became `indexOf(…) === -1` the moment `title`,
232+
// a `text` field, moved to `table.text` — which reads as a passing
233+
// "less than" only until you notice what it is less than.
234+
expect(ts.indexOf("table.string('id')")).toBeLessThan(ts.indexOf("('title')"));
235+
expect(ts.indexOf("('title')"), 'the title column vanished from the output').toBeGreaterThan(0);
230236
// Each generator carries exactly ONE hardcoded id line — the shape that let
231237
// these two disagree with the driver in the first place, and the reason a
232238
// fix to one of them can silently leave the other behind. Counted over the

packages/cli/src/commands/generate-field-type-vocabulary.pin.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,18 @@ describe('#14828 — the SQL answers are the platform’s, not this file’s inv
567567
expect(tsInterfaceType('number')).toBe('number');
568568
// The driver's own answer for the headline member, read where it lives.
569569
expect(createColumnArm('autonumber')).toContain('table.string(name)');
570-
expect(sqlColumn('autonumber')).toBe(sqlColumn('text'));
570+
// #16091 — compared against `lookup`, not against `text`. Both were
571+
// `VARCHAR(255)` when this line was written, which made `text` a usable
572+
// stand-in for "the driver's default string column"; it is not one any
573+
// more. `createColumn` gives `text` its text-family arm (an unbounded TEXT
574+
// for every unkeyed column) and gives `lookup` the same bare
575+
// `table.string(name)` it gives `autonumber` — asserted here, from the
576+
// driver, so the comparator cannot silently become a different question again.
577+
expect(createColumnArm('lookup')).toContain('table.string(name)');
578+
expect(sqlColumn('autonumber')).toBe(sqlColumn('lookup'));
579+
// Anti-vacuity: the comparator is a real, DIFFERENT answer from the
580+
// text family's, so this equality is a measurement rather than a tautology.
581+
expect(sqlColumn('autonumber')).not.toBe(sqlColumn('text'));
571582
expect(tsColumn('autonumber')).toBe("table.string('f_autonumber')");
572583
});
573584

packages/cli/src/commands/generate-multiple-json-column.pin.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,14 @@ describe('#14829 — `multiple: true` is one answer across all three surfaces',
165165
// control cannot be satisfied by one column shape for everything either.
166166
expect(sqlColumn('single_lookup')).toBe('VARCHAR(255)');
167167
expect(tsColumn('single_lookup')).toBe("table.string('single_lookup')");
168-
expect(sqlColumn('single_text')).toBe('VARCHAR(255)');
169-
expect(tsColumn('single_text')).toBe("table.string('single_text')");
168+
// #16091 — `text` is an unbounded TEXT column now, which is what
169+
// `createColumn`'s text-family arm builds for every unkeyed column. The
170+
// control is unweakened by that for exactly the reason the `lookup` note
171+
// above gives: what it discriminates is scalar-vs-JSON, and TEXT is scalar.
172+
expect(sqlColumn('single_text')).toBe('TEXT');
173+
expect(tsColumn('single_text')).toBe("table.text('single_text')");
174+
// …and it still discriminates: the scalar answer is not the JSON one.
175+
expect(sqlColumn('single_text')).not.toBe(sqlColumn('multi_text'));
170176
expect(sqlColumn('single_file')).toBe('VARCHAR(2048)');
171177
expect(tsInterfaceType('single_lookup')).toBe('string');
172178
});
@@ -285,8 +291,8 @@ describe('#14829 — `multiple: true` is one answer across all three surfaces',
285291
// this card does NOT touch is present in both outputs. Without it, "does
286292
// not contain" would pass on an empty string.
287293
expect(other).toContain('CREATE TABLE IF NOT EXISTS "probe" (');
288-
expect(other).toContain('"t" VARCHAR(255)');
289-
expect(otherTs).toContain("table.string('t')");
294+
expect(other).toContain('"t" TEXT');
295+
expect(otherTs).toContain("table.text('t')");
290296

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

0 commit comments

Comments
 (0)