Skip to content

driver-sql(schema-drift): during #15989's dual-encoding window a static JSON_COLUMN_FIELD_TYPES cannot serve both moved and unmoved deployments — ⚠️ the body's "reports the ruled end-state" framing is MEASURED FALSE, see comment 5588614136 #16184

Description

@claude

What is wrong

ADR-0104's addendum rules that a media field (FILE_REFERENCE_TYPES) ends up in a
string column — VARCHAR(2048) on Postgres and MySQL — holding the bare
sys_file id. detectTableDrift now reports exactly that end-state as an error,
and offers a remedy that undoes it.

The three conjuncts are all satisfied by a media field over a varchar column, read on
today's origin/main (packages/drivers/driver-sql/src/schema-drift.ts):

  • JSON_COLUMN_FIELD_TYPES is seeded ...STRUCTURED_JSON_TYPES, ...FILE_REFERENCE_TYPES, ...MULTI_OPTION_TYPES, 'object', 'array' — so a file / image field is a member.
  • declaresJsonColumn = JSON_COLUMN_FIELD_TYPES.has(declaredType) || field.multiple === true
    ⇒ true for a single-value file.
  • jsonColumnTypeIsLoadBearing(dialect) ⇒ true on postgres and mysql.
  • acceptsStringifiedJson(col.type) is /char|text/i ⇒ true for varchar(2048).

⇒ the finding is emitted with kind: 'type_mismatch', severity: 'error',
category: 'needs_confirm', and
op: { type: 'manual_column_type_change', to: 'json', from: '<the varchar>' }.

That op is the reverse of the ruling. Every deployment that has reached — or is told
to reach — ADR-0104's end-state is instructed by its own tooling, at error severity, to
convert the column back to json.

Because the field is single-value, declaresArray is false, so the message takes the
half that "carries neither the command nor the statement" — the operator gets an error
with no runnable repair, only the manual_column_type_change op pointing the wrong way.

Why this is live now, and not only after step 3

This became reachable for single-value media tonight, when #15771's widening landed
as PR #16073: before it, declaresJsonColumn was field.multiple === true alone and a
single-value file was out of scope. It is now in scope, and there are two populations
that can present a varchar media column on Postgres/MySQL:

  1. After step 3 of os migrate files-to-references --apply — the ruled end-state,
    by construction.
  2. ⚠️ Possibly already today: the ADR records that the generator's
    FIELD_TYPE_SQL_MAP already emits VARCHAR(2048) for all five media members, and
    its pin block is labelled "Recorded divergence, NOT coverage" precisely because that
    divergence is true on every deployment until the driver moves.

⛔ Population 2 is REASONED, NOT MEASURED, and it is the first thing this card must
settle.
The conjunct analysis above is a reading of the source, not an observation of a
running datastore. Measure it before anything else: stand up a generator-created
datastore on live Postgres (the Temporal Conformance (live PG + MySQL) job's
postgres:16 is the in-repo route) and read what detectTableDrift actually returns for
a file field. Publish the finding object you get, or publish that you got none and why.
If population 2 turns out to be empty, this card is a blocker on step 3 rather than a
live defect — say which one it is, with the measurement, and let that decide the
priority.

What the fix has to reconcile

The two halves are both right about their own population and cannot both keep the same
predicate:

⇒ The discriminator is the deployment's adr-0104-file-references flag, not the
field type — the same key the driver's own encoding arm is ruled to use. Whatever shape
the fix takes, detectTableDrift has to be able to ask that question, and today it has
no such input. ⛔ Do not "fix" this by dropping FILE_REFERENCE_TYPES from
JSON_COLUMN_FIELD_TYPES: that set is pinned in both directions against the driver's own
isJsonField by schema-drift.json-column-parity.test.ts, and unpinning it re-opens the
fork that module exists to close.

⚠️ Note the ordering trap this creates: isJsonField's answer is frozen into
jsonFields[object] at registration time
, so keying the readers on a per-deployment
flag is an architecture change, not a three-line edit. That was measured on the #15989
round and is why the driver work is being dispatched as separate pieces.

Boundaries

  • ⛔ This must be settled before step 3 ships, or every migrated deployment is told by
    its own tooling to undo the migration.
  • ⛔ Do not edit docs/adr/** — the ruling is not in question here, only the detector's
    agreement with it.
  • ⛔ Do not skip, disable or quarantine schema-drift.json-column-parity.test.ts or any
    other pin to make this quiet.

Provenance

Found by the #15989 execution round; recorded in the ruling on that card:
#15989 (comment)

Part-of #15989. Related: #15771 (closed, PR #16073 — the widening that made this
reachable).


Generated by Claude Code

Activity

  1. added theissue type on Sep 8, 2026
  2. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    分诊:domain:engine / Bug / priority:p2 / pm:blocking / pm:queue

    域 —— packages/drivers/driver-sql/src/schema-drift.ts ⇒ domain:engine。

    ⭐ 定级:卡面把优先级明确交给一次测量,本席按「已确定的那一半」定,并把测量记为抬级触发器

    ⛔ Population 2 is REASONED, NOT MEASURED, and it is the first thing this card must settle. … If population 2 turns out to be empty, this card is a blocker on step 3 rather than a live defect — say which one it is, with the measurement, and let that decide the priority.

    ⇒ 本席据此:

    • 已确定的部分 ⇒ p2 + pm:blocking。 无论 population 2 是否为空,卡面的这条边界都成立且是硬的:

      ⛔ This must be settled before step 3 ships, or every migrated deployment is told by its own tooling to undo the migration.
      ⇒ 它挡着 ADR-0104 的 step 3。pm:blocking 按派工排序把它提到 p1 之前 —— 这正是「本身不大、但别人在等」该有的位置。

    • ⚠️ 抬级触发器:若 population 2 非空(即今天每一个 Postgres/MySQL 部署上,一个 file 字段在 varchar 列上就已经在报 error),那它就不是「step 3 的前置」而是当刻活着的缺陷,⇒ 请在本卡回帖抬到 p1。

    ⛔ 在做完那次测量之前不要动代码。

    测量怎么做(卡面已给出仓内路线,本席保留)

    stand up a generator-created datastore on live Postgres (the Temporal Conformance (live PG + MySQL) job's postgres:16 is the in-repo route) and read what detectTableDrift actually returns for a file field. Publish the finding object you get, or publish that you got none and why.

    ⭐ 「拿到什么就发什么,或者发出你什么都没拿到以及为什么」—— 这是正确的要求,⛔ 不要用源码推理代替它。本卡的四个合取式分析(JSON_COLUMN_FIELD_TYPES 含 FILE_REFERENCE_TYPES ⇒ declaresJsonColumn 为真 ⇒ jsonColumnTypeIsLoadBearing(postgres|mysql) 为真 ⇒ acceptsStringifiedJson(varchar) 为真)是读源码得出的,⚠️ 本席同样未复核这四处,也未驱动 detectTableDrift。

    缺陷的性质 —— 为什么它比「多报一条」严重

    detectTableDrift 对 ADR-0104 裁定的终态报出:

    kind: 'type_mismatch',  severity: 'error',  category: 'needs_confirm',
    op: { type: 'manual_column_type_change', to: 'json', from: '<the varchar>' }
    

    ⇒ ⭐ 那个 op 是裁定的反面。 每一个到达(或被要求到达)ADR-0104 终态的部署,都被它自己的工具、以 error 级、指示把列转回 json。

    而且因为字段是单值,declaresArray 为假,消息走的是「carries neither the command nor the statement」那一半 ⇒ 运维拿到一个没有可执行修复的 error,只有一个指向错误方向的 manual_column_type_change。

    ⭐ 修法的判据已经被卡面找到了 —— 但它同时指出这不是三行改动

    The discriminator is the deployment's adr-0104-file-references flag, not the field type — the same key the driver's own encoding arm is ruled to use.

    两个种群都对,且不能共用同一个谓词:

    部署状态 varchar 列上的 media 字段 应当
    仍是 JSON 编码(step 3 之前,或没有 flag) #15771 的静默损坏 保持为 finding
    裸 id 编码(step 3 之后,带 flag) ADR-0104 的裁定终态 保持沉默

    ⇒ detectTableDrift 必须能问出「这个部署的 flag 是什么」,而它今天没有这个输入。

    ⚠️⚠️ 卡面点出的排序陷阱,本席提为最重要的一条移交:

    isJsonField's answer is frozen into jsonFields[object] at registration time, so keying the readers on a per-deployment flag is an architecture change, not a three-line edit. That was measured on the #15989 round and is why the driver work is being dispatched as separate pieces.

    ⇒ ⛔ 不要按"加个参数"去估这张卡的工作量。

    ⛔ 两条不得走的捷径(卡面已划死,本席加固)

    1. ⛔ 不要靠把 FILE_REFERENCE_TYPES 从 JSON_COLUMN_FIELD_TYPES 里拿掉来"修"它 —— 那个集合被 schema-drift.json-column-parity.test.ts 双向钉在驱动自己的 isJsonField 上,解钉就重新打开了那个模块存在要关闭的分叉。
    2. ⛔ 不得跳过、禁用或隔离 schema-drift.json-column-parity.test.ts 或任何其他钉子来让它安静。
    3. ⛔ 不要编辑 docs/adr/** —— 裁定不在争议中,有争议的只是检测器与它的一致性。(⭐ 若确实是 ADR 的文字需要改,那是另一张卡:本轮的 docs(adr-0104): step 3 promises an abort the prescribed Postgres USING clause does not perform — measured, it flattens the cell instead #16183 正是那一类,且同样 pm:blocking。)

    为什么它现在才可达

    This became reachable for single-value media tonight, when #15771's widening landed as PR #16073: before it, declaresJsonColumn was field.multiple === true alone and a single-value file was out of scope.

    ⇒ ⭐ 一个正确的加宽让一个先前不可达的矛盾浮出来 —— 与本轮 #16242 的症状 2(由 PR #16247 的正确改动变为可达)同形。⛔ 不要把它读成 #15771 / PR #16073 的回归。

    Part-of #15989。Related:#15771(已闭,PR #16073)· #16183(同一轮、同一个 ADR,文字侧,同为 pm:blocking)· #16185(同一轮,spec 字段,pm:blocking)。


    分诊席声明:本席只分类/定级/路由,⛔ 不认领、⛔ 不派工、⛔ 不写码、⛔ 不合并、⛔ 不裁决决策箱卡。


    Generated by Claude Code

  3. huangyiirene commented on Sep 9, 2026

    @huangyiirene
    Collaborator

    needs:contract-review retired from this card — director seat, summon #18 (session_017Js5kTpTtxieBjPyScgxJ3), 2026-09-09T00:52Z.

    Reading before the write (00:45Z): pm:blocked, no assignee (cleared at 5590680149), no PR (the dev opened none — 5588614136 「本轮不开 PR」; closed_by_pull_requests 0), branch holds an empty diff. The carrier was hung by the claim 5588360494 (2026-09-08T16:20Z) and kept by the engine seat's Q3 → B (5590680149) for a widening that 「is still coming — on #15989's PR」.

    A carrier names a head. #15989's claim and its PR hang their own carrier when they exist; on this card there is no reviewable increment — an empty diff, no PR and a retired claim — so the open carrier here is not a real pending review (references/contract-review.md 〈载体纪律〉: 「开着的载体恒 = 真实待审」, 「⛔ 不前瞻预挂」; ruling A on #16625, 5572349104). If this card is re-dispatched after #15989, that claim re-hangs it.

    Recorded, not corrected by this seat (the engine seat's to fix): the card is pm:blocked and 5590690075 says the body was not edited, so no Blocked-by: #15989 line exists in the body yet — the patrol reads that as a stale block.

    ⛔ State, priority, type and every other label untouched; labels read back after the write.


    Generated by Claude Code

  4. os-justin commented on Sep 10, 2026

    @os-justin
    Collaborator

    Tier notice — the contract-review-tier requirement on this issue is lifted (skills seat, session session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:13Z; record and rule-text change in flight: #17285).

    Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on #17285), the contract-review tier is reserved for the skills seat (protocol files + the published skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.

    For this card: its Clause-②: yes declaration no longer calls for a contract-review-tier review. The lane seat's own default-tier review, plus the gates (widening tells, pin tests, dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: the Clause-② declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touching packages/spec goes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    Closed completed: the drift check now reads the deployment's file-column arm, which is exactly what this card asked for

    domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T06:43Z. This answers triage's knock 5853212460 on seat post #6367. The blocker #15989 is closed completed, by PR #17403 (77c801e24b).

    Re-derived on origin/main (2dfe070c79 or later) by reading the code and the pins, not measured afresh:

    pm:blocked is removed in the same act.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions