Uh oh!
There was an error while loading. Please reload this page.
fix(driver-sql): varchar differ expects what createColumn would emit (#12732) - #12774
Conversation
…le (#12732) Recovered from a killed prior run; committing before merge/further work to avoid losing it to another container restart.
…rchar-differ-expected-width
…12732) Thread SqlDriver.varcharColumnChars (the emitter's own read-only mirror) and indexedKeyColumns() into diffManagedTable's varchar-length branch, so an unkeyed bounded text-family field and a base string-family field bounded past MAX_VARCHAR_CHARS stop being expected as varchar(N) when createColumn would never build one. Adds test coverage at both the differ level and the SqlDriver wiring level, plus a changeset.
…nally Mutation: replace the emitterWouldVarchar ternary in schema-drift.ts's varchar branch with an unconditional 'true' (ABLATION_12732 marker), reverting the branch to pre-fix always-fire behaviour whenever a declaredMaxLength exists, regardless of what the emitter mirror says. Predicted flips, PASS -> FAIL (6 of 16 across the two new test files): schema-drift.12732-varchar-emitter-parity.test.ts (4): - Case A block: 'stops firing narrow_varchar once the emitter mirror is threaded, on both enforcing dialects' - Case A block: 'a field NOT in the keyedColumns map is treated as unkeyed, not as unknown' - Case B block: 'stops firing widen_varchar above MAX_VARCHAR_CHARS (16383), on both enforcing dialects' - additive-default block: 'omitting keyedColumns alone (mirror threaded) reads every field as unkeyed' sql-driver-12732-varchar-emitter-parity-wiring.test.ts (2): - 'an UNKEYED bounded text-family field reports no narrow_varchar (Case A, fixed)' - 'a base string-family field past the varchar ceiling reports no widen_varchar (Case B, fixed)' Predicted to STAY GREEN (10): every 'still fires' / boundary / keyed / already-agreeing / SQLite / mirror-control test, and both additive-default tests that omit varcharColumnChars entirely — none of them read the mutated line.
tsc caught the mismatch: varcharColumnChars expects (field: FieldDef, keyed?) => number | null, and Record<string, unknown> is not assignable to FieldDef (no index signature). Full driver-sql typecheck is now clean.
…rchar-differ-expected-width
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. 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 bb03798e95afda30fc18d382262eefc83bd8e32f && git checkout bb03798e95afda30fc18d382262eefc83bd8e32f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 87d3f9a0aa7e180b8ff785f05b851a3b9d4ffdb2 1d0e5f1e6451baa573002358ea78cd32f76c87bb && git checkout -B drift-repro 87d3f9a0aa7e180b8ff785f05b851a3b9d4ffdb2 && git merge --no-ff 1d0e5f1e6451baa573002358ea78cd32f76c87bb
node scripts/docs-audit/affected-docs.mjs --json 87d3f9a0aa7e180b8ff785f05b851a3b9d4ffdb2 |
os-zhuang
commented
Aug 27, 2026
PM review — ACCEPT on substance; enqueueing on all-greenReviewer of record: The fence triage set, measured
Zero, in both directions. The category was not touched, and — as triage predicted — it did not need to be: the predicate makes Case A stop firing, so there was never a Every suppression is paired with a case proving the branch still firesThis is what I check hardest on a "stop reporting X" fix, because the cheap version of it guts the branch and passes:
Sixteen cases, and the boundary pair ( Clause ② was reverse-verified, not asserted
That proves the widened type actually reaches consumers through built output — the thing a stale The ablation discriminates the right thingForcing Restore proven by On the resumeThis card lost an agent to a container restart with hours of uncommitted work in a surviving worktree. The salvage was the right call and the report's handling of it — re-reading the inherited diff rather than trusting my half-read of it, then committing and pushing at every checkpoint — is what made the second run cheap. The Enqueueing once every check is green, not the required subset. Generated by Claude Code |
os-zhuang
commented
Aug 27, 2026
PM addendum — the docs were already right, and they corroborate this fixThe drift bot listed nothing here, and on this PR that silence is not self-justifying the way it was on #12757: this change does alter user-visible behaviour, so "no page can be falsified" is not available as an argument. The bot also names its own blind spot, and it lands squarely on this territory — "a page that states a rule by its inputs shares no identifier with the emitter", measured on So I read that page by hand. It turns out to describe both of this card's cases, correctly, and to have done so all along.
That is Case A, stated exactly — including the keyedness condition your fix threads through
That is Case B, ceiling and rationale both. What this means for the review. The differ was not disagreeing only with It is also a clean example of the bot's structural limit rather than a bug in it: the page names Control for the grep, so the zero is a reading: No change requested. Still enqueueing on all-green. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12732
What
The managed-schema drift differ's varchar-length branch expected
varchar(field.maxLength)for any bounded field, over a pre-existing column, on dialects that enforce varchar length (postgres/mysql).SqlDriver.createColumndoes not build that for every bounded field, and the two disagreed in two measured directions:text/richtext/signature/markdown/ …) reportednarrow_varcharat severityerror, categorydestructive— the one categoryrunArtifactBootMigrationGaterefuses a boot for — demanding the column be narrowed to a shapecreateColumnwould never build (unkeyed, it leaves the column TEXT). Trigger: an ordinary edit, addingmaxLength: 50to a legacyvarchar(255)text column on an already-serving deployment.email/url/password/ …) bounded pastSqlDriver.MAX_VARCHAR_CHARS(16383) reportedwiden_varcharatwarning/safe, planningALTER … varchar(100000)— DDL MySQL refuses outright (ERROR 1074), while Postgres accepts it silently.This guard had already been patched at the call site three times for the same defect class (#11431 for
multiple: true; #11794/#11875 for genuine TEXT columns). Rather than a fourth patch, the branch now asksSqlDriver.varcharColumnChars(field, keyed)— the emitter's own read-only mirror ofcreateColumn's switch, already pinned againstcolumnInfo()for everyFieldType— what widthcreateColumnwould actually build.nullmeans "the emitter would not make this a varchar," and the branch does not fire.Keyedness (
indexedKeyColumns(), #11374) is threaded fromSqlDriver.detectTableDriftintodiffManagedTable, since a KEYED bounded text-family field legitimately takesvarchar(maxLength)— the fix suppresses the false positive, it does not gut the branch. A keyed field over the same shape still reports.Scope
Per triage's ruling (
5442400497) and the PM claim (5442549411): the category question is out of scope. Fixing the predicate makes Case A stop firing entirely, so there is nothing left to downgrade — changing the category as a separate act would be gate-weakening. Confirmed in this PR: the predicate fix alone clears both cases; the category question never came up.Premise re-measurement
Both cases were re-driven directly through
diffManagedTableagainstorigin/main(pre-fix, via a temporarygit checkout origin/main --on the two touched files inside this branch, restored and hash-verified after) before writing any fix code:text/richtext/signature/markdown,maxLength: 50overvarchar(255), postgres):narrow_varchar/error/destructive/ expectedvarchar(50)— reproduced exactly as the issue states.email,maxLength∈ {100000, 16384, 16383} overvarchar(255), postgres):widen_varchar/warning/safe/ expectedvarchar(N)(N = the declared maxLength) for all three — reproduced exactly.Positive control:
manual_widen_varchar_to_textgreps 2 onorigin/main'sschema-drift.ts(the #12121 op this file also carries), confirming the read was against the real ref, not a stale one.Tests
packages/drivers/driver-sql/src/schema-drift.12732-varchar-emitter-parity.test.ts— differ-level, directdiffManagedTablecalls: both cases suppressed onpostgres/mysql; the KEYED counter-case still fires at the emitter's width; the ceiling boundary (16383fires,16384does not); the additive default (omitting the new params keeps pre-driver-sql: the varchar differ's expected width disagrees with whatcreateColumnwould emit — a boot-refusingnarrow_varcharon a bounded text field, and avarchar(n)past the dialect ceiling #12732 behaviour unconditionally); unaffected shapes stay silent.packages/drivers/driver-sql/src/sql-driver-12732-varchar-emitter-parity-wiring.test.ts—SqlDriver.detectTableDriftwiring: provesindexedKeyColumns()(from a field-leveluniqueand from a declared composite index) and the boundvarcharColumnCharsactually reachdiffManagedTable, withintrospectColumns/introspectIndexesmocked so this stays a unit test of the wiring rather than a live-DB test.Ablation
Prediction committed before mutating (
a43f9027c): forcingemitterWouldVarchar = trueunconditionally (markerABLATION_12732) should flip exactly 6 of the 16 new tests from PASS to FAIL — the 4 "stops firing" / "treated as unkeyed" assertions in the differ-level file and the 2 "reports no …" assertions in the wiring file — while the 10 "still fires" / boundary / keyed / unaffected-shape / mirror-control tests (the positive controls) stay green.Measured: exactly those 6 failed, by name, and the other 10 passed. Mutation confirmed on disk before running (anchored grep: target text 1→0, marker 0→1); restore confirmed after (
git diff HEADempty,git hash-object== HEAD blob, marker residue 0); restored tree re-run green (16/16).Cross-package check (clause ②)
diffManagedTableis exported frompackages/drivers/driver-sql's publicindex.ts, and its args object gains two optional parameters — a public-surface widening, additive only. Reverse-verified against the rebuilt.d.ts(not a cache): a scratch probe inpackages/cli(a real consumer ofdiffManagedTable) passing a wrong type forvarcharColumnCharswas rejected bytsc(TS2322, citing the real parameter type), then removed and confirmed clean again.packages/cli's owndiffManagedTableconsumer tests (artifact-boot-migration.report-only-drift,multi-value-columns.dry-run,multi-value-columns.dialect-probe) pass unchanged (18/18) — no existing caller changes shape, matching the additive-default test in this PR.Gates
Derived via
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, re-derived after the final commit (1d0e5f1e6,HEAD=origin/mainmerge). All matched local gates green:check:changeset-gate-self-tests,check:cross-package-test-inputs,check:driver-conformance,check:objectql-double-limit,check:objectui-changeset,check:page-declaration-shape,check:published-files,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check-adr-0087-registration,check-changeset-no-major,check-ci-filter-parity,check-comment-mask-adoption,check-cross-package-test-inputs(script),check-empty-changeset,check-plugin-teardown-shape,check-affected-docs,check-drift-comment,release-rehearsal-clone --self-test,check:query-options-erasure,check:type-check-coverage,check:type-check-debt(full workspace build first: 70/70 tasks; re-measure: 1687 raw errors, none above ledger),check:engine-double-contract,check:where-matcher.check:pm-half-states(root gate, not the driver-conformance one) exited 3 — PREREQUISITE NOT MET, no validGITHUB_TOKENin this container (anonymous path also unusable:x-ratelimit-remaining: 0). NOT MEASURED, not a failure; unrelated to this diff.pnpm --filter @objectstack/driver-sql test: 2195 passed, 129 skipped (live PG/MySQL cells — noOS_TEST_POSTGRES_URL/OS_TEST_MYSQL_URLin this container; visible skips, not silent), 0 failed.pnpm --filter @objectstack/driver-sql typecheck: clean.Changeset
minor, argued in.changeset/varchar-differ-expects-what-createcolumn-emits.md— mirroring #12121'sminorgrade in the opposite direction: an already-serving deployment that currently fails to boot over Case A will boot after this upgrade, and awiden_varcharcurrently eligible for dev auto-reconcile over Case B will no longer be planned. Both are user-visible behaviour changes for an existing deployment, not merely an internal correctness detail. The exported-args widening is additive and, unlike #12121'sDriftOpunion member, adds no case an exhaustive consumer switch must handle — not itself a reason to grade higher.Generated by Claude Code