Uh oh!
There was an error while loading. Please reload this page.
fix(driver-sql): name the declared field a builtin column discards, on all three DDL paths (#12015) - #12109
Conversation
…n all three DDL paths
`initObjects` emits `id`, `created_at` and `updated_at` itself and skipped any
declared field colliding with one in silence — the declared type, length and
constraints were dropped with no diagnostic anywhere. Measured on PostgreSQL
16.13 and again here on SQLite: a declared `id: { type: 'text', maxLength: 12 }`
lands as `varchar(255)`.
Every path that drops such a declaration now warns once per colliding field,
naming the field, the object and the platform's ownership: the CREATE branch,
the ADD COLUMN diff (a stock upgrade's path), and the rotation shard sync.
Each path carries its own call and its own pin so a regression to a silent
`continue` on one path fails by name.
⛔ Not a rejection door: the accept set is untouched and the DDL emitted is
byte-identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o📓 Docs Drift CheckThis PR changes 1 package(s): 74 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f4bdc0e653993a75f62c7e0677720ccd52c62558 && git checkout f4bdc0e653993a75f62c7e0677720ccd52c62558
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 494279cb31f1d92adab959763085e19c923a8652 059b7789d806bb4a17c7a70ac51780d53ed555a2 && git checkout -B drift-repro 494279cb31f1d92adab959763085e19c923a8652 && git merge --no-ff 059b7789d806bb4a17c7a70ac51780d53ed555a2
node scripts/docs-audit/affected-docs.mjs --json 494279cb31f1d92adab959763085e19c923a8652
|
…ion discards
The first cut warned on every declared field colliding with `id` /
`created_at` / `updated_at`. Measured against the real corpus that was not
merely noisy but FALSE: 116 warnings on a stock boot of platform-objects
alone, against declarations like `id: Field.text({ label: 'Presence ID',
required: true, readonly: true })` whose label IS applied (four generated
locales, highlightFields, FLS, sortability) and whose `required` IS enforced
(ADR-0113 write contract). "The declaration is NOT applied … remove the
declaration" was untrue there, and following it would have deleted an
author-facing label.
The trigger is now "asks for storage the platform's own column does not
deliver", decided by one classification table pinned against
`FieldSchema.shape`, and the message names the lost attributes and what the
column really is instead of denying the whole declaration. `id: { type:
'number' }` and `id: { type: 'text' }` still fire.
⛔ Not route C and not a rejection door: the platform still owns the column
and the declaration still does not take effect. This changes what we say.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6oUh oh!
There was an error while loading. Please reload this page.
Fixes#12015
initObjectsemitsid,created_atandupdated_atitself and then skipped any declared field colliding with one —if (builtinColumns.has(name)) continue;, with no warning, no throw and no record anywhere that the author's declaration had been discarded. The driver is right to own its primary key and audit stamps; the defect was that it disagreed with the author in silence.Two maintainer rulings, both 2026-08-25, and the second is why this PR looks the way it does:
Nothing here moves an accept/reject door or widens a public surface. The platform still owns these three columns and the declaration still does not take effect: what changed is what the driver says.
Why the narrowing, in one example
Against
sys_presence.id = Field.text({ label: 'Presence ID', required: true, readonly: true }), the first shape said "the declaration is NOT applied … Remove the declaration". Both halves of that are wrong there: thelabelis applied (it feeds the generated*.objects.generated.tstranslation files in four locales,highlightFields, FLS and sortability),requiredis enforced (ADR-0113 makes it the write contract, not a column constraint), and an author who followed the advice would have deleted the author-facing label for a column every list view shows. Only the storage half is discarded — so only the storage half is reported now.What changed
packages/drivers/driver-sql/src/builtin-column-collision.ts(new) — the split, in one place:FIELD_KEY_STORAGE_CLASSclassifies everyFieldSchemakey asstorage(the DDL layer would have read it:type,maxLength,unique,defaultValue,storage.notNull,multiple,expression,precision…) orpresentation(honoured elsewhere:label,readonly,searchable,required,autonumberFormat, the ADR-0010 provenance markers…). The line is drawn at what the DDL reads, and the load-bearing entry isrequired— ADR-0113 makes it the write contract, so calling it discarded is exactly the false sentence this narrowing removes.BUILTIN_COLUMN_DELIVERYrecords what the platform's own columns actually provide, read off the emitting lines:id=table.string('id').primary()(varchar(255), NOT NULL, unique, no default);created_at/updated_at=createAuditTimestampColumn(a timestamp defaulted to the DB clock, left nullable).undeliveredStorageAttributes(column, field)returns only what the declaration asks for and the column does not provide. At runtime an unclassified key is silent — a diagnostic must never invent a warning it cannot justify — and the exhaustiveness pin, not a throw, is what makes an unclassified key visible.sql-driver.ts— onewarnBuiltinColumnCollisionshelper, called from each of the three paths that drop such a declaration:initObjects, before theCREATE TABLEwhile creating table "…"initObjects, existing-table branch — the builtin is already there, so the diff never proposes it; the path a stock upgrade takeswhile syncing existing table "…"ensureShardTable, ahead of both its create and column-sync brancheswhile syncing shard "…"The message now names the attributes rather than denying the declaration:
id: { type: 'number' }— the author error the card was filed for — still fires, as does the #11456 fixture'sid: { type: 'text' }.created_at: { type: 'datetime', defaultValue: 'NOW()' }describes precisely what lands, and says nothing.Measurements
The defect, re-measured rather than recalled. The card measured PostgreSQL 16.13 (declared
id: text→varchar(255)). The accept-set pin measures SQLite:id: { type: 'text', maxLength: 12 }lands asvarchar(255)with the bound binding nothing, while a non-collidingregion: { type: 'text' }gets its declaredtext. MySQL was not measured — no live cell in this container — so the card's MySQL gap stands, explicitly.The corpus, re-measured after the narrowing with the same real boot (all 45 object definitions exported by
@objectstack/platform-objects, driven throughSqlDriver.initObjectson SQLite, 45/45 booting green):id45,created_at42,updated_at29idonly — every oneid: Field.text(…)against avarchar(255)key;sys_migration.idalso declares amaxLengththat binds nothingThe 71 that disappeared are the
created_at/updated_atdeclarations that describe exactly whatcreateAuditTimestampColumnemits. ⛔ The 45 that remain were not tuned away: they are the same disagreement the ruling insists must keep firing, on the platform's own metadata. They are named individually and filed as #12131 (unassigned) — a metadata question forplatform-objects, not something to fix from inside this card's band.Ablation, five legs (
be08012875, predictions written before each run, direction RED throughout — these are presence/absence pins). Mutation proven on disk by anchored counts of the exact text being changed, before any result was read;packages/drivers/driver-sql/distwas absent and both pin files import from inside the package, so vitest readsrc/with no rebuild able to hide a mutation.'shard'call1 failed | 16 passed—× SHARD path: …'create'call3 failed | 14 passed— exactly those'alter'call1 failed | 16 passed10 failed | 7 passed— 9 as predicted, plus the unknown-key case, which the prediction missed and which is correct: its fixture declares atypethe mutation now reportsrequiredasstorage3 failed | 14 passed— those two plus the module's presentation-silence caseperlsubstitution never matched, the on-disk proof read0 → 0, and no result was read from it; it was redone with a mutation that provably lands (marker0 → 1) and the table above reports that run. Every leg restored undertrap … EXIT INT TERMwithgit checkout HEAD --given the absolute path, then verified:git hash-objectequalledgit rev-parse HEAD:for both files (db4846b…,3e78a6c…) andgit status --porcelainwas empty after each leg and at the end.Verification
Gate union derived at the final commit, not recalled:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackatbe08012875(6 paths vs merge base8450eeacb) — 14 matched families + 6 convention-triggered by the test files. Every one run in the foreground underscripts/pm/os-verify-lock.sh, exit code captured before any pipe. All 23 green:pnpm --filter @objectstack/driver-sql exec vitest run—Tests 2028 passed | 121 skipped (2149),GATE_EXIT=0pnpm --filter @objectstack/driver-sql typecheck(tsc --noEmit) —GATE_EXIT=0check:nul-bytes—OK (scanned 6711 text file(s) … no raw ASCII control bytes)check:driver-conformance—OK — 45 covered cell(s), 0 in the DEBT ledger, 0 exemptcheck:test-source-alias—OK — 72 packages with tests scannedcheck:cross-package-test-inputs—OK: 16 package(s) read outside themselves, all declaredcheck:where-matcher—299 matcher(s) … 0 silently-wrong … none newcheck:type-check-debt— closure built first exactly aslint.ymldoes (70 successful, 70 total), because its refusal means NOT MEASURED:--re-measure: OK — 32 ledger entr(ies) re-measured in 234.1s, 1897 raw tsc error(s) total, none above its recorded numbercheck:published-files,check:slot-lookup,check:type-source-resolution,check:changeset-gate-self-tests,check:objectui-changeset,check:query-options-erasure,check:type-check-coverage,check:engine-double-contract,check-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check-plugin-teardown-shape,release-rehearsal-clone --self-test,docs-audit/check-affected-docs,docs-audit/check-drift-commentChangeset:
patchon@objectstack/driver-sql, graded in the file itself — the emitted DDL is byte-identical, no public type or metadata key moves, and the only observable difference is a log line for storage that was already being discarded.Generated by Claude Code