You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
os migrate multi-value-columns has no arm for a SINGLE-value JSON-class column — its repair wraps every value in a one-element array, so the new #15771 findings are deliberately routed to remedy_not_recognized instead #16072
Found while implementing #15771 (widening the base-type drift detector to the writer's own JSON-class predicate). Filed unassigned; no severity asserted.
The gap
manualJsonConversionSql (packages/drivers/driver-sql/src/schema-drift.ts) repairs a stale textual column by WRAPPING each stored value in a one-element JSON array — the ELSE json_build_array(col) arm on Postgres, UPDATE ... SET col = JSON_ARRAY(col) WHERE LEFT(col, 1) is not a bracket on MySQL. That is the correct repair for a field whose declared value IS an array, and the wrong one for a field whose declared value is a scalar or an object.
Measured with the command's own planner (planStaleColumnTargets, packages/cli/src/commands/migrate/multi-value-columns.ts) before #15771's message split, on a synthesized single-value file finding carrying the multi-value message:
targets: 1 refusals: 0
dialect: postgres
ALTER TABLE "proj_task" ALTER COLUMN "doc" TYPE json USING (CASE WHEN "doc" IS NULL THEN NULL
WHEN "doc" = '' THEN NULL WHEN "doc" LIKE '[%' THEN "doc"::json
ELSE json_build_array("doc") END)
The stored value for a single-value file field is the JSON scalar the write path produces — measured byte-for-byte on an in-memory SQLite cell, the column holds "file_01HXYZ" including its quotes, identically for the stale varchar(2048) column and the driver's own column. Wrapping that yields a one-element array whose element still carries the quotes. Read back through the driver on the same cell:
ok : value="file_01HXYZ" typeof=string isArray=false
wrapped : value=["\"file_01HXYZ\""] typeof=object isArray=true
(The wrap itself is the repo's own live measurement, recorded in manualJsonConversionSql's docblock from Postgres 16.13 and MySQL 8.0.46: a legacy single value becomes ['a']. There is no live Postgres/MySQL cell in this container, so that limb is quoted, not re-measured.)
What #15771 shipped instead, and what is still missing
#15771 does NOT point the new findings at that command. The array-valued half (multiple: true, plus the inherently-multi option types) keeps its message character for character and keeps the remedy. The single-value half gets a message carrying neither the command's name nor the statement, so planStaleColumnTargets recovers no dialect and REFUSES the entry with remedy_not_recognized — the command's own designed branch for a message it cannot read. Nothing wrong is executed, and the refusal's detail already tells the operator to run os migrate plan and apply the finding's statement by hand.
What is still missing is an automated repair for the single-value population, and the finding's message says so in words rather than printing DDL nobody has run. The shape of that repair is a CAST rather than a wrap — rows written through ObjectStack already hold valid JSON text — but writing it is not a change that should ship unmeasured: manualJsonConversionSql's two existing forms are EXECUTED against live servers by schema-drift.base-type-mismatch.test.ts over rows in every state a stale column can be in, and its own docblock records two arms that were corrected only by running an earlier version against a live database.
Options for whoever picks this up
Extend the existing command with a value-shape arm — one command, two statement builders, the shape chosen from the finding rather than from the command's name. Costs the command's name accuracy ("multi-value" would no longer describe what it does).
A sibling command for the scalar/object population, leaving os migrate multi-value-columns exactly as it is. Costs an operator one more thing to know about.
Found while implementing #15771 (widening the base-type drift detector to the writer's own JSON-class predicate). Filed unassigned; no severity asserted.
The gap
manualJsonConversionSql(packages/drivers/driver-sql/src/schema-drift.ts) repairs a stale textual column by WRAPPING each stored value in a one-element JSON array — theELSE json_build_array(col)arm on Postgres,UPDATE ... SET col = JSON_ARRAY(col) WHERE LEFT(col, 1) is not a bracketon MySQL. That is the correct repair for a field whose declared value IS an array, and the wrong one for a field whose declared value is a scalar or an object.Measured with the command's own planner (
planStaleColumnTargets,packages/cli/src/commands/migrate/multi-value-columns.ts) before #15771's message split, on a synthesized single-valuefilefinding carrying the multi-value message:The stored value for a single-value
filefield is the JSON scalar the write path produces — measured byte-for-byte on an in-memory SQLite cell, the column holds"file_01HXYZ"including its quotes, identically for the stalevarchar(2048)column and the driver's own column. Wrapping that yields a one-element array whose element still carries the quotes. Read back through the driver on the same cell:(The wrap itself is the repo's own live measurement, recorded in
manualJsonConversionSql's docblock from Postgres 16.13 and MySQL 8.0.46: a legacy single value becomes['a']. There is no live Postgres/MySQL cell in this container, so that limb is quoted, not re-measured.)What #15771 shipped instead, and what is still missing
#15771 does NOT point the new findings at that command. The array-valued half (
multiple: true, plus the inherently-multi option types) keeps its message character for character and keeps the remedy. The single-value half gets a message carrying neither the command's name nor the statement, soplanStaleColumnTargetsrecovers no dialect and REFUSES the entry withremedy_not_recognized— the command's own designed branch for a message it cannot read. Nothing wrong is executed, and the refusal's detail already tells the operator to runos migrate planand apply the finding's statement by hand.What is still missing is an automated repair for the single-value population, and the finding's message says so in words rather than printing DDL nobody has run. The shape of that repair is a CAST rather than a wrap — rows written through ObjectStack already hold valid JSON text — but writing it is not a change that should ship unmeasured:
manualJsonConversionSql's two existing forms are EXECUTED against live servers byschema-drift.base-type-mismatch.test.tsover rows in every state a stale column can be in, and its own docblock records two arms that were corrected only by running an earlier version against a live database.Options for whoever picks this up
os migrate multi-value-columnsexactly as it is. Costs an operator one more thing to know about.type_mismatchfinding is keyed tofield.multipleonly, 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 ships.Either of the first two needs a live Postgres and MySQL cell to land, for the same reason the existing forms did.
Related: #15771 (the detector), #15041 (the
VARCHAR(2048)-vs-json-column generator divergence that makes the stale column reachable).