Skip to content

An operator-run command to migrate a stale column to json when a field went multi-value (ruled C on #11700) #11733

Description

@huangyiirene

Filed by the domain:engine seat (session session_01VK8rFDtg8eREaxBGX99Csn) as the execution half of the #11700 ruling.

The ruling this card executes

Maintainer, 2026-08-24, live PM chat. ⛔ Quoted verbatim, not translated:

「11700 11693 不需要考虑历史数据,其他按照你的建议继续」

Ruled: C — the platform warns and ships an explicit, operator-run migration command. ⛔ It is never run automatically at boot. Option A (unattended auto-migration) is rejected: on the four facets it was the only route that has the platform altering a customer's production table structure with nobody watching. B (warn only) survives solely as C's degenerate form if this command is never shipped — which is what this card exists to prevent.

⚠️Amendment, from the ruling's own words — historical data is OUT of scope. The #11700 analysis carried a confidence gap flagging that no route repairs rows corrupted before detection landed. That rider is struck: rows already corrupted are the customer's to repair, as the reporting customer already did. ⛔ Do not add backfill or row-repair to this command, and ⛔ do not file a card for it.

What to build

A command an operator runs deliberately, on their own schedule, having taken their own backup. The shape the ruling names:

  • the migration itself — the dialect-correct statement for moving a stale varchar/text column to json when its field declares multiple: true;
  • a dry run — show exactly what would run, and against what, without running it;
  • rollback notes — what the operator does if it goes wrong;
  • docs — so the finding's message can point at something real.

⚠️The SQL is already written and, more importantly, already measured. PR #11720 (the detection half) emits the remedy statement per dialect, and its suite executes that statement against live Postgres 16.13 and MySQL 8.0.46 over four row states, asserting the finding then clears. ⛔ Do not re-derive it — reuse it, and read the two corrections that were forced by measurement rather than preference:

  • to_json(col) turns a legacy single value into a JSON scalar under a now-multi-value field (Array.isArray === false), so json_build_array is used instead, which makes Postgres agree with MySQL's JSON_ARRAY;
  • json_build_array(NULL) is [null] — a one-element array — so an explicit IS NULL arm exists. It was added after the version without it was run on the live server and observed to give every NULL row a value.

Dialect scope, measured: the corruption occurs on Postgres and MySQL and does not on SQLite (which reads back a real array). SQLite is excluded.

Before dispatching, re-verify rather than assume

  • The detection half must have landed: check that PR fix(driver-sql): report a multi-value field left on a stale varchar/text column #11720 is merged and that manual_column_type_change is emitted on origin/main
    (git grep -n "manual_column_type_change" origin/main -- packages/drivers/driver-sql/src/schema-drift.ts).
  • ⚠️ Confirm where the command belongs. The drift finding lives in packages/drivers/driver-sql, but an operator-facing command plausibly lives in packages/cli next to the other os migrate surfaces — which is another seat's lane. If it lands there, this becomes a cross-lane card and goes to that queue, ⛔ not into a driver-sql PR. Establish the landing point before writing code; it may change this card's domain:*.
  • ⛔ The reconciler must still not execute this op automatically. fix(driver-sql): report a multi-value field left on a stale varchar/text column #11720 ships manual_column_type_change with no reconciler arm by design; this command is a separate, deliberate path the operator invokes. A change that quietly gives the op an arm would convert C back into A, which was rejected.

Related

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions