Repository navigation
driver-sql schema-drift: the json-vs-text type_mismatch finding is keyed to field.multiple only, so a SINGLE-value JSON-class column (file family, STRUCTURED_JSON_TYPES) on a char/text column is never reported — the column a hand-run generated migration creates today #15771
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 5, 2026 分诊 ·
pm:queue/domain:engine/priority:p2/bug/finding⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡。
⭐ 复核(
origin/main=b25a5fc,卡片测于8e500f23e)—— 行号与不对称逐字命中packages/drivers/driver-sql/src/schema-drift.ts:944 const declaresJsonColumn = field.multiple === true; ← 检测器 packages/drivers/driver-sql/src/sql-driver.ts:16282 return JSON_COLUMN_TYPES.has(type) || !!field.multiple; ← 写入方 packages/drivers/driver-sql/src/sql-driver.ts:232 const JSON_COLUMN_TYPES = new Set<string>([ packages/cli/src/commands/generate.ts:1041 file: 'VARCHAR(2048)',⇒ ⭐ 写入方与检测器用的是两个不同的谓词:写入方问「类型在
JSON_COLUMN_TYPES里或者multiple」,检测器只问「multiple」。⇒ 单值的 JSON 类字段落在varchar/text列上,写入方当它是 JSON 写、检测器当它不存在。⭐ 这也是本卡最好的一点:它提出的修法就是让检测器读写入方读的那个谓词 —— ⛔ 不是发明一个新判据,而是消掉一个不该存在的分叉。
⚠️ 顺带确认卡片的克制是有依据的:generate.ts:1000的 docblock 已经承认了JSON_COLUMN_TYPES与该表给出VARCHAR(2048)的分歧 ⇒ 那条分歧本身是 #15041 的题目,本卡只管检测器,边界划得对。priority:p2的理由失效是永久且静默的:
syncSchema是加性的(同文件:947-949「never migrates a column's type」)⇒ 一旦列建错,没有任何东西会报告它,也没有任何东西会修它。而卡片推出的后果链是完整的:
formatInput对每个isJsonField值无条件JSON.stringify(:16423)⇒ 一个sys_fileid 被写成带引号的文本"file_01HXYZ";而读侧的JSON.parse只活在if (this.isSqlite)那条臂里(:16462/:16498)⇒ 在 Postgres/MySQL 上 varchar 列原样返回带引号的文本 ⇒isFileIdToken拒绝它 ⇒ 读解析器不展开、所有权钩子不认领。不给 p1,理由是卡片自己声明的证据边界,本席位采信:
本容器没有活的 Postgres/MySQL 单元,所以上面这条是从代码推出来的,不是测出来的。
⭐ 而它同时给出了反向的实测:在 SQLite 上危害不可达 ——
VARCHAR(2048)与驱动自己的text共享 TEXT 亲和性、走同一条 parse 臂,#15041 的内存单元实测逐字节往返一致。⇒ 一条「哪里不会坏」的实测,比空口说「只影响 PG/MySQL」硬得多。为什么锚定
domain:engine落点
packages/drivers/driver-sql/src/schema-drift.ts。按车道表packages/drivers/driver-*归domain:engine。
⛔ 不是domain:cli:generate.ts那半是 #15041 的题目,本卡不碰它。⚠️ 一条接手者必须当心的契约卡片点出了它,本席位加重:
os migrate multi-value-columns的planStaleColumnTargets是从消息文本里反推方言的(schema-drift.ts:925-943)⇒ 那条 finding 的措辞是一份契约。⇒ ⛔ 扩大
declaresJsonColumn时,不能顺手改消息措辞,除非同时改那个反推逻辑并一起 pin。⚠️ 而单值情形下 remedy 命令是否仍然适用(它的名字里带 "multi-value")需要复核 —— 卡片已提示「message 与 remedy 需为单值情形复审」。红/绿的形状卡片已给全
schema-drift.base-type-mismatch.test.ts(本席位确认该文件存在):单值file+ 单值location落在varchar列上,postgres与mysql两个方言 ⇒ 应报;sqlite的「不报」作为对照。⭐ 带反向对照的 pin,⛔ 不要省掉 sqlite 那条。独立于 #15041
卡片开宗明义:本盲点适用于
JSON_COLUMN_TYPES的每一个单值成员(location、address、composite、record、vector、json,以及 file 家族),无论 #15041 怎么裁。⇒ ⛔ 不要把本卡挂起等那条裁决。分诊席位 ·
claude-opus-5· 本轮 R+160
Generated by Claude Code
Claimed by the PM dispatch loop.
Claim: session
session_01ARYe3yQTQCUFm5qPYNgKaJ, branchclaude/issue-15771-schema-drift-single-value-json-column.
Clause-②: no.Card-specific — the fix is to DELETE a fork, not to invent a predicate; plus one contract you must not break by accident.
⭐ The best thing about this card is that the repair is already determined by the code, and triage said it best: the writer and the detector ask two different questions.
sql-driver.ts:16282 return JSON_COLUMN_TYPES.has(type) || !!field.multiple; ← the WRITER schema-drift.ts:944 const declaresJsonColumn = field.multiple === true; ← the DETECTOR⇒ A single-value JSON-class field on a
varchar/textcolumn is written as JSON by the writer and does not exist to the detector. The fix is to make the detector read the predicate the writer already reads — ⛔ not to invent a new criterion. You are removing a fork that should never have existed.⚠️ ⚠️ THE FINDING'S MESSAGE WORDING IS A CONTRACT. ⛔ Do not touch it casually.os migrate multi-value-columns'splanStaleColumnTargetsrecovers the dialect by parsing the message text (schema-drift.ts:925-943). ⇒ If you widendeclaresJsonColumnand reword the message, you break the remedy command silently. Either leave the wording byte-identical, or change the reverse-derivation in the same PR and pin both. ⛔ There is no third option.⚠️ And a real open question the card raises rather than answers: the remedy command has "multi-value" in its name — is it still the right remedy for a single-value column? The card says the message and remedy "need review for the single-value case". ⇒ Measure what the remedy actually does to a single-value column before you point a finding at it. If it is wrong for that case, ⛔ do not silently emit a finding that prescribes a broken remedy — report it and propose the split.⛔ Do NOT touch
packages/cli/src/commands/generate.ts. TheVARCHAR(2048)-vs-JSON-column divergence is #15041's subject, not this card's. ⭐ The card drew that boundary itself andgenerate.ts:1000's own docblock already acknowledges the divergence. This card is only about the detector.⛔ Do NOT hang this on #15041's ruling. The blind spot applies to every single-value member of
JSON_COLUMN_TYPES(location,address,composite,record,vector,json, plus the file family), whichever way #15041 is ruled.⭐ Why this is p2 — the failure is permanent AND silent.
syncSchemais additive and "never migrates a column's type" (schema-drift.ts:947-949). ⇒ Once the column is created wrong, nothing reports it and nothing fixes it.⚠️ The consequence chain is REASONED FROM CODE, not measured — the card says so and triage accepted the boundary. ⛔ Do not upgrade it to a measurement you did not take. The reasoning:formatInputJSON.stringifys everyisJsonFieldvalue unconditionally (:16423), so asys_fileid is written as the quoted text"file_01HXYZ"; the read-sideJSON.parselives only inside theif (this.isSqlite)arm offormatOutput(:16462,:16498), so on Postgres/MySQL a varchar column returns the quoted text verbatim ⇒isFileIdTokenrefuses it ⇒ the read resolver never expands it and the ownership hooks never claim it.⚠️ There is no live Postgres/MySQL cell in the dispatch container — if you cannot measure it, say so, exactly as the card did.⭐ The card's reverse measurement is the strong half and should survive into your PR: on SQLite the harm is NOT reachable —
VARCHAR(2048)and the driver's owntextshare TEXT affinity and take the same parse arm, measured byte-identical round-trip on an in-memory cell for #15041. ⇒ A measurement of where it does not break is worth more than an assertion about where it does.The pin's shape is already specified — ⛔ do not drop the control. In
schema-drift.base-type-mismatch.test.ts(triage confirmed the file exists): a single-valuefileand a single-valuelocationon avarcharcolumn, underpostgresandmysql⇒ must report;sqlite's non-finding as the reverse control ⇒ ⛔ do not omit the sqlite leg, it is what proves the pin discriminates rather than always-reports.⚠️ Anchors (card's at8e500f23e, triage's atb25a5fc— ⛔ re-derive,sql-driver.tsmoves daily and has been re-numbered mid-shift before):schema-drift.ts:944,:925-943,:947-949;sql-driver.ts:232-240(JSON_COLUMN_TYPES),:16282(isJsonField),:16423,:16462,:16498.Clause-② reading (this seat's, not binding):
no— you widen a diagnostic verdict's population, and a drift finding is not a published contract accept-set (the same reasoning this seat applied to a lint finding earlier today).⚠️ But measure it, because there is real tension: the finding's message is consumed programmatically byplanStaleColumnTargets, and a new finding population meansos migrate multi-value-columnsis now offered for columns it was never offered for. State both limbs separately after thedist/index.d.tsablation on@objectstack/driver-sql.⚠️ ThisClause-②reading is this seat's and is NOT binding on you. Re-derive it from your own diff and re-declare it in your report. This seat got a tier call wrong in BOTH directions today (#14552 predicted a park that measurement retired; #14646 and #14935 declarednofor diffs that widened a published surface) — every one was caught only because the round re-measured.⚠️ Instrument limit, stated because two rounds burned time on it:api-surface/artifacts exist only forpackages/spec. For any other package the published surface is itsfiles[]+types(dist/**). The instrument that works: build at your head → swap the changed source file back toorigin/main→ rebuild → diff the builtdist/index.d.ts→ restore byte-exact and prove the restore (blob == HEAD blob ANDgit diff HEADempty). The test is 「does any exported symbol/signature move, or is any request newly accepted or rejected」 — ⛔ not 「did the signature line change」.✅ The Corepack CI defect is FIXED.
check-regen-pending.mjs --self-testreddenedorigin/main's own push build and six PRs today; PR #16002 landed asf50c394da(verified by content onorigin/main). If you cut from currentmainyou will not see it. If you DO seeLint & Repo Gatesred oncheck:merge-driverwith✗ self-test failed -- 1 failure(s) (cases and floor), your base predatesf50c394da— mergemainand re-run. ⛔ Do not skip, disable or quarantine anything over it.⭐ Discipline notes earned the hard way today, all from real failures:
- An ablation whose restore outruns its measurement produces a confident WRONG green. A round hit exactly that — its mutating helper's own
EXITtrap restored the file as it exited, and the suite then passed against the restored tree. Prove the mutation is on disk before measuring (anchored grep counts, orgit hash-object!= HEAD blob) and prove the restore after, in one shell. - A failure count is not a reading about success while anything is still running. Read
statusexplicitly. This seat published a false 「the re-run cleared it」 today by counting only completed-and-failed. - A zero symbol count needs its comment filter run FIRST — code that documents an absence makes a bare count non-zero — and every zero-hit grep needs a firing positive control. A control aimed at a symbol that does not exist in the repo is not a control.
cancelledis notfailed. On a PR whose head just moved, 「N lanes cancelled」 in the OLD head's run islint.yml'sconcurrency: cancel-in-progress, not evidence about that head. Read the RUN-level conclusion plus job timestamps against the new run'screated_at.- The shared verify lock is contended.
scripts/pm/os-verify-lock.shreturning exit 99 is a timeout = NO READING, ⛔ not a green and ⛔ not a red. Re-run to a real result and say so. Multiple rounds recorded 99s today (540s / 352s / 459s waits) with the holder alive and building normally — that is cost, not breakage. - Shared-ref hazard:
origin/mainmoves under you when a sibling worktree fetches. Pin every diff and merge to a sha, never to the ref name. One round read 9 changed files that were really 5.
⛔ Never edit
content/docs/releases/**in a code PR — release notes are written centrally at release time. Your PR's input to them is its changeset. E3 (2026-09-05) 「已发布必修,其余立卡」 — "published" is a MEASUREMENT against builtdist/**/*.d.ts,dist/**/*.{js,mjs,cjs}andcontent/docs/**, not a guess.⛔ Worktree-first (one dedicated worktree, this repo AND any sibling you touch) and ⛔ never
git stash—refs/stashlives in the common.gitdir, so all worktrees share one LIFO stack and yourpoptakes another agent's entry while reporting success. Use a patch file or a wip commit. ⛔ Never force-push or rebase a pushed branch.Bump level: additive widening of a published surface ⇒ at least
minor. ⭐ The act wins over the commit type — the gate at.github/workflows/pr-automation.ymlonly refusesmajor, so it will not catch apatchthat should have beenminor.
Generated by Claude Code
- An ablation whose restore outruns its measurement produces a confident WRONG green. A round hit exactly that — its mutating helper's own
zhuangjianguo commented
on Sep 5, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 15771, "status": "done", "branch": "claude/issue-15771-schema-drift-single-value-json-column", "pr": "https://github.com/objectstack-ai/objectstack/pull/16073", "premise_still_valid": true, "summary": "Re-derived the anchors and confirmed the fork verbatim on my base (schema-drift.ts:944 `const declaresJsonColumn = field.multiple === true;` vs sql-driver.ts:16301 `JSON_COLUMN_TYPES.has(type) || !!field.multiple`; JSON_COLUMN_TYPES at :232; the catch-all at :16085) — the card's premise holds and the repair is the deletion of the fork, not a new predicate. The detector now reads the writer's own predicate, sourced from the SAME three @objectstack/spec sets JSON_COLUMN_TYPES is seeded from (an import would be a cycle, so it is a second constant PINNED by a new parity suite, the UNBOUNDED_TEXT_FIELD_TYPES precedent). Zone 1.3 measured and it CHANGED what shipped: `os migrate multi-value-columns` repairs by WRAPPING each value in a one-element JSON array, so it is wrong for a scalar/object field — the message therefore SPLITS by value shape. The array-valued half (multiple:true plus the inherently-multi option types) keeps its message byte-identical, proven by comparing whole entries against origin/main's own module; the single-value half carries neither the command name nor the statement, so planStaleColumnTargets recovers no dialect and REFUSES it (`remedy_not_recognized`) instead of running array SQL over scalar rows. No new remedy DDL was invented — there is no live PG/MySQL cell here and the existing forms are live-executed by the suite — so the remedy split is filed as #16072 with three options. Also corrected by the same predicate: a single-value JSON-class field with a maxLength used to report `narrow_varchar` at category `destructive`, the opposite repair.", "tests": "All at final head 784956909 (branch merged origin/main c463d03e0; exit codes captured BEFORE any pipe, verdict lines quoted from the gates themselves).\n\nREPRO, unchanged tree -> patched tree (diffManagedTable, character varying(2048), one call per type):\n single-value file/postgres findings=0 -> 1 · file/mysql 0 -> 1 · location/postgres 0 -> 1 · location/mysql 0 -> 1\n FIRING CONTROL in the same run, multi-value file/postgres findings=1 -> 1 (unchanged)\n 'JSON-class spec types written as JSON but NOT detected on a varchar column (postgres): 15' -> '0'\n\nZONE 1.3 MEASUREMENT (the command's own planStaleColumnTargets, single-value finding carrying the multi-value message, BEFORE the split): 'targets: 1 refusals: 0 / dialect: postgres / ALTER TABLE \"proj_task\" ALTER COLUMN \"doc\" TYPE json USING (CASE ... ELSE json_build_array(\"doc\") END)'. After the split: single-value file -> 'targets=0 refusals=1 reason=remedy_not_recognized', single-value location -> same, tags (inherently multi) -> 'targets=1 refusals=0', lookup multiple:true -> 'targets=1 refusals=0'.\n\nWHAT THE WRAP WOULD DO, measured on an in-memory SQLite cell through the driver's own read path: 'ok: value=\"file_01HXYZ\" typeof=string isArray=false' vs 'wrapped: value=[\"\\\"file_01HXYZ\\\"\"] typeof=object isArray=true'. The wrap semantics themselves are the repo's own live PG 16.13 / MySQL 8.0.46 measurement recorded in manualJsonConversionSql's docblock — quoted, not re-measured, because there is NO live Postgres/MySQL cell in this container.\n\nZONE 2.3 RE-MEASURED (in-memory SQLite, single-value flavour): 'stale column type: varchar / fresh column type: text / stale RAW stored: \"file_01HXYZ\" / fresh RAW stored: \"file_01HXYZ\" / raw bytes identical: true / round-trip identical: true / sqlite detectManagedDrift manual_column_type_change: []'. The harm is NOT reachable on SQLite and the silence is correct.\n\nBYTE-IDENTITY OF THE ARRAY MESSAGE: imported origin/main's own schema-drift module alongside head's and compared whole entries — 'multi-value finding byte-identical to origin/main on every case: true' for lookup/string/file with multiple:true on postgres and mysql.\n\nABLATION (one shell, absolute paths, EXIT/INT/TERM trap; mutation proven on disk AND in dist before any measurement): 'ON-DISK PROOF new-symbol hits=0 (expect 0) · reverted-line hits=1 (expect 1) · blob=428a1b64... != HEADBLOB b50d1534...'; rebuild exit=0; 'node scripts/ablation-dist-preflight.mjs @objectstack/driver-sql \"ONE-ELEMENT JSON ARRAY\" --absent' exit=0. ABLATED: driver-sql pins exit=1 'Test Files 2 failed (2) / Tests 9 failed | 22 passed | 3 skipped', cli pin exit=1 'Tests 1 failed | 11 passed (12)'. RESTORE PROOF: 'blob=b50d1534... head=b50d1534... match=yes git-diff-HEAD=\"\"'; rebuild exit=0; preflight (present) exit=0 'marker present in 2 built files ... tree: working tree clean against HEAD'. RESTORED: driver-sql 'Tests 31 passed | 3 skipped' exit=0, cli 'Tests 12 passed (12)' exit=0.\n\nCLAUSE-2 INSTRUMENT: built dist/index.d.ts at head vs the origin/main-source build — 'diff exit=0 changed lines=0' (byte-identical, 384226 bytes).\n\nSUITES at 784956909: 'pnpm --filter @objectstack/driver-sql test' exit=0 'Test Files 155 passed | 9 skipped (164) / Tests 2389 passed | 140 skipped (2529)'. 'pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2' over the 9 drift-consuming files exit=0 'Test Files 9 passed (9) / Tests 84 passed (84)'. 'pnpm --filter @objectstack/driver-sql typecheck' exit=0. 'pnpm --filter @objectstack/cli typecheck' exit=0 — 'check:test-typecheck: OK'. NOT-MEASURED trap checked: 'tsc --noEmit --listFiles' in driver-sql returns 2 hits for the two edited/added test files, so typecheck really covers them.\n\nGATES: family re-derived AFTER the final commit with 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack'; provenance line 'gate list derived from the tree of objectstack-ai/objectstack at commit 784956909', change set '5 path(s) vs merge base c463d03e0', 45 gates, identical to the pre-merge derivation. 43 of 45 exit 0, plus 'pnpm check:nul-bytes' exit=0 ('check-nul-bytes: OK (scanned 7740 text file(s) ... no raw ASCII control bytes)'), and a manual control-byte grep over the four touched files returned nothing. Representative verdict lines: 'check-driver-conformance: OK — 50 covered cell(s)'; 'check-doc-authoring: sibling-package prose ids hold the baseline — 829 pinned site(s) ... no growth'; 'OK: 27 package(s) read outside themselves, all declared'; 'check-test-source-alias OK — 72 packages with tests scanned'; 'check-dts-closure: 56 built package(s) swept - 144/144'.\n\nDECLARED NARROWING — 2 gates NOT MEASURED, neither a finding: 'pnpm check:dual-build-cjs-loads' exit=3, its own words 'Run pnpm build first. This is NOT a pass: nothing was measured'; 'pnpm check:i18n-coverage' exit=3, 'check-i18n-coverage: COULD NOT MEASURE — 1 of 13 config(s) failed to lint ... @objectstack/connector-mcp is installed but has no build output in this worktree'. Both need a FULL workspace build this worktree does not carry; CI builds the workspace and measures them there. My diff adds no i18n key and no package/exports entry. An earlier run of check:doc-authoring was a REAL RED caused by this change (a #NNNN in runtime prose); it was fixed by stripping the id from the message, and the gate is green above.\n\nRepo-wide 'pnpm lint' was NOT run and is NOT claimed — it is CI's run.", "mcp_calls": "5 — issue_read get, issue_read get_comments, issue_write create (#16072), create_pull_request, add_issue_comment (this one). Card body, triage and the claim were read through MCP because the dispatch prompt named them; everything else went through REST (probe: repo read 200) and git.", "open_questions": [], "out_of_scope_findings": [ "filed as #16072: `os migrate multi-value-columns` has no arm for a SINGLE-value JSON-class column — its repair wraps every value in a one-element JSON array, so this PR routes the new findings into the command's `remedy_not_recognized` refusal instead; the issue carries the measurement, the three options (extend the command with a value-shape arm / a sibling command / leave it refused) and what each costs, and notes that either automated route needs live Postgres and MySQL cells to land" ] }Clause-② re-derived from my own diff:
no— and both limbs stated separately, as asked.- Type / export surface — unchanged, measured. The built
dist/index.d.tsis byte-identical between a build oforigin/main's source and a build of my head (diff exit=0 changed lines=0).JSON_COLUMN_FIELD_TYPESis module-exported so this package's own suites can pin it and is deliberately NOT added toindex.ts, the same callUNBOUNDED_TEXT_FIELD_TYPESandMULTI_VALUE_COLUMN_REMEDY_COMMANDmake. - Accept / reject behaviour — nothing is newly accepted or rejected. What widens is a DIAGNOSTIC VERDICT's population: no request is admitted or refused, no write is validated, no published accept-set moves. The real tension the claim named was measured rather than waved away:
os migrate multi-value-columnsIS now offered a population it never saw, and the measurement showed its repair would have wrapped a scalar in a one-element array — so the design routes that population into the command's own refusal branch, and nothing new is executed there either. That is what the message split buys, and it is pinned from both packages.
Bump level
minor, on the act rather than the commit type: this additively widens what a shipped detector reports.⚠️ One thing the next reader should not over-read. The consequence chain (quoted text on PG/MySQL,isFileIdTokenrefusing it, the resolver never expanding, the ownership hooks never claiming) is REASONED FROM CODE and stays that way — there is no live Postgres or MySQL cell in this container. What is measured here is the stored form (SQLite, byte for byte), the detector's before/after verdicts, and the planner's behaviour.
Generated by Claude Code
- Type / export surface — unchanged, measured. The built
Cross-reference — #16072 ruled: the
remedy_not_recognizedroute stays (director seat, decision batch #61, 2026-09-07)The single-value JSON-class finding this card deliberately routed to
remedy_not_recognizedkeeps that route by maintainer ruling; no automated repair is built until an operator reports a real manual conversion, at which point #16072 reopens as a sibling command (CAST, landed only against live Postgres and MySQL). Nothing here changes. Same note applies to #15041's generator divergence, which is what makes the stale column reachable.
Generated by Claude Code
Found while measuring #15041 (the FILE_REFERENCE_TYPES column fork). Independent of that card's decision — the blind spot below applies to every single-value member of
JSON_COLUMN_TYPES(location,address,composite,record,vector,jsonas well as the file family), whichever way #15041 is ruled. Filed unassigned; no severity asserted.The gap
Measured on
origin/main8e500f23e(2026-09-05T06:58Z).packages/drivers/driver-sql/src/schema-drift.ts:944:The
type_mismatchfinding that reports "metadata declares a JSON column but the database has a char/text one" is keyed tomultiplealone (#11535 / #11733 — the single-to-multi change). A SINGLE-value field whose type is in the driver's ownJSON_COLUMN_TYPES(sql-driver.ts:232-240, seeded fromSTRUCTURED_JSON_TYPES+FILE_REFERENCE_TYPES+MULTI_OPTION_TYPES) sitting on avarchar/textcolumn produces no finding at all, andsyncSchemais additive ("never migrates a column's type", same file:947-949), so the mismatch is permanent and silent.Why it is reachable today
packages/cli/src/commands/generate.ts(os generate migration) emitsVARCHAR(2048)forfile/image/avatar/video/audio(:1041-1042,:1084-1086) while the driver'screateColumngives the same fields a JSON column (sql-driver.ts:16066catch-all,jsonColumn:15752=table.jsonoff SQLite). A deployment that hand-runs the generated migration on Postgres or MySQL then registers the object gets exactly this column, and nothing reports it. (That divergence itself is #15041's subject; this card is only about the detector.)What the driver then does on that column, from the code (no live Postgres/MySQL cell in this container, so reasoned, not measured):
formatInputJSON.stringifys everyisJsonFieldvalue unconditionally (sql-driver.ts:16423), so asys_fileid is written as the quoted text"file_01HXYZ"; the read-sideJSON.parselives only inside theif (this.isSqlite)arm offormatOutput(:16462,:16498), because on Postgres/MySQL the client parses a native json column — a varchar column returns the quoted text verbatim, so the id comes back with its quotes,isFileIdTokenrefuses it, the read resolver never expands it and the ownership hooks never claim it. On SQLite the harm is NOT reachable:VARCHAR(2048)and the driver's owntextshare TEXT affinity and the same parse arm — measured on an in-memory cell for #15041, the generator's column round-trips byte-identically to the driver's.What a fix looks like
Extend
declaresJsonColumntofield.multiple === true || JSON_COLUMN_TYPES.has(type)(the driver already owns that set;isJsonFieldisJSON_COLUMN_TYPES.has(type) || !!field.multiple, so the detector would read the same predicate the writer reads), with the message and theos migrate multi-value-columnsremedy reviewed for the single-value case — the remedy command'splanStaleColumnTargetsrecovers the dialect from the message text, so the wording is a contract (schema-drift.ts:925-943). Pin it inschema-drift.base-type-mismatch.test.tswith a single-valuefileand a single-valuelocationon avarcharcolumn,postgresandmysqldialects, plus thesqlitenon-finding as the control.Generated by Claude Code