Uh oh!
There was an error while loading. Please reload this page.
fix(driver-sql): the stale multi-value column warning names os migrate multi-value-columns - #12012
Conversation
…y command The finding that reports a multi-value field left on a stale varchar/text column opened its remedy with "ObjectStack will NOT change this column for you. Migrate it by hand" and then printed raw SQL. That became false when `os migrate multi-value-columns` shipped: there is now an operator-run command that does exactly this, with a dry run as the default, a prompt, and a post-run re-detection that exits non-zero if the finding has not cleared. Operators were being sent to hand-write DDL on a production table while the safer route sat one command away, unnamed. The message now leads with the command and keeps the hand-run statement after it. Both surfaces print `message` verbatim, so the boot warning and `os migrate plan` both pick it up. Unchanged, deliberately: severity `error` + category `needs_confirm`. The artifact boot gate refuses a boot on category === 'destructive' and nothing else, and every database this finding describes is already serving. No load-time or write-time refusal was added. The dialect statement stays embedded VERBATIM — a contract, not formatting: a ManagedDriftEntry carries no dialect, so the CLI recovers one by testing which dialect's statement the message contains. Now pinned from the emitting side too. 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): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
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 f86b364b7fc8d483ff63a5f4f95751f95c82822d && git checkout f86b364b7fc8d483ff63a5f4f95751f95c82822d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2cc71222459e91964e883419611a820c28302429 765f07814eb2078bde87cbcb40d1057d6265f3d3 && git checkout -B drift-repro 2cc71222459e91964e883419611a820c28302429 && git merge --no-ff 765f07814eb2078bde87cbcb40d1057d6265f3d3
node scripts/docs-audit/affected-docs.mjs --json 2cc71222459e91964e883419611a820c28302429
|
Uh oh!
There was an error while loading. Please reload this page.
Part of #11535
The remaining half of the card, unblocked by
os migrate multi-value-columns(#11733,0e5bea6). The detection half landed in #11720; this makes the finding it emits point at the command that now exists.The defect, measured on
origin/mainbefore touching anythingThis is the line an operator meets on every restart of an affected database — captured through the real boot path (
reconcileAndWarnDrift→ logger), on live Postgres 16.13, atd63b014360:Two things are wrong with it, and neither is cosmetic:
os migrate multi-value-columnsfalsified it. The message tells the operator the command two lines below it does not exist.The change
The message leads with the command and keeps the hand-run statement after it, for an operator without the CLI:
Both operator-facing surfaces pick this up without touching either:
renderPlan(packages/cli/src/utils/schema-migrate.ts:442) printsd.messageverbatim, soos migrate planshows it, and so does the boot warning.⛔ What was NOT changed, deliberately
severity: 'error',category: 'needs_confirm'— untouched. Boot-gating is decided bycategory:runArtifactBootMigrationGaterefuses a boot oncategory === 'destructive'and nothing else, atkernel:readybefore the socket opens. Every database this finding describes is already serving — that is the premise of the user's report — so making the report louder must never become the outage. Pinned by the existing category case, which the diff leaves alone.planStaleColumnTargets(packages/cli/src/commands/migrate/multi-value-columns.ts:154) recovers the dialect by testing which dialect's statement the message contains:A
ManagedDriftEntrycarries no dialect. So a reword that paraphrased, wrapped, or line-broke the SQL would make every findingremedy_not_recognized— the command this message now recommends would refuse to run, and nothing indriver-sql's own suite would have noticed. That is now pinned from the emitting side (keeps the statement VERBATIM, because the CLI recovers the dialect by containment, which reproduces the probe rather than describing it), and the risk is written into the emission site's comment.Verified rather than reasoned:
driver-sqlrebuilt, then the CLI's threemulti-value-columnssuites run against thatdist— 21 passed.P0 confirmation (Zone 1b) — it still reproduces
Triage flagged this P0-suspect and left the engine seat to confirm. Re-measured end-to-end on live Postgres 16.13 at
d63b014360, through the operator path (single-value table → metadata gainsmultiple: true→initObjects→ write):character varying(255)while a fresh DB of the same metadata getsjson;["x","y"](typeof === 'string',Array.isArray === false).The corruption is still reachable. What has changed since the report is that it is no longer silent (#11720) and is now repairable by a command (#11733) — the "silent" in "silent data corruption" is gone; the reachability is not, and removing it would mean refusing the write or the boot, which this dispatch fences off and the #11700 ruling settles. Reported plainly rather than as a footnote.
Non-vacuity, both directions
A signal that fires on everything and one that never fires read equally green, so both are pinned:
Fires — unit (both dialects), and on a live Postgres boot: the line names the table, the physical type, the dry run,
--apply, and carries the statement intact through the logger. Also pinned: the command appears before the raw SQL, so an operator who stops reading at the firstALTER TABLEhas already passed it.Does not fire — six shapes enumerated (migrated
jsoncolumn · single-value field · single-value narrow and widen width drift · SQLite · staleinteger), asserting no message names the command and nomanual_column_type_changeop appears; two of those rows do produce a finding, asserted, so the loop reads real messages rather than passing over empty arrays. On a live database: after the remedy runs, a restart must stop recommending it.Ablation
Direction predicted in advance, mutation proven on disk before any result was read, restore under
trap … EXIT INT TERM.${MULTI_VALUE_COLUMN_REMEDY_COMMAND}interpolation removed from the message (anchor verified unique: 1 occurrence).1 → 0, injected marker0 → 1. The script aborts withexit 90if either count is wrong.Tests 3 failed | 21 passed | 2 skipped— exactly those three../schema-drift.js) from inside its own package, so vitest resolvessrc/. The source was mutated with no rebuild and the suite went red — had it stayed green, that would have been the stale-distsignature. Not a dist-leg ablation, so the strippable-marker caveat does not apply.0, anchor1,diffagainst the pre-mutation copy byte-identical,git statusclean.Verification
Gate union derived, not recalled:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(asserted against this checkout'sorigin; 3 paths vs merge base). 14 path-matched + 6 convention-triggered families, all run, exits captured before any pipe. All green, plus:pnpm --filter @objectstack/driver-sql typecheck(tsc --noEmit) — 0pnpm lint— whole repo,eslint . --no-inline-config, 0. Not narrowed.99 passed | 2 skipped24 passed | 2 skipped(was 18 passed before this change)dist—21 passedDeclared narrowings, both with their measurement:
check:type-check-debt --re-measure— not run; needs the whole workspace closure built, and it re-runs tsc per ledger entry.driver-sqlis in neither theEXEMPTnor theTEST_DEBTblock ofscripts/check-type-check-coverage.mjs(measured here, not inherited), so this change cannot move a ledgered count. The structural half passed, andtsc --listFilesshows both changed files inside the package's own program, which exits 0.mysqldis not installed in this container, so that matrix cell is a named skip. Postgres carries the live legs; CI runs both.All 20 were read at the final commit
765f07814ein one run, with a clean working tree asserted in the same run (ALL GATES ABOVE READ AT: 765f07814e · working tree 0 change(s)). An earlier attempt was cut off mid-run by the container's 10-minute foreground cap under lock contention; it is not the run reported here.Generated by Claude Code