What is wrong
ADR-0104's --apply R4 block promises that step 3 moves a media column while
aborting on the first cell that is not a JSON string
and the paragraph immediately below prescribes the realisation:
Postgres retypes the column to a varchar with a USING clause that unquotes the JSON string
Measured on live PostgreSQL 16.13, those two sentences contradict each other. The
USING clause does not abort — it converts. With one un-backfilled inline-object row
present, ALTER TABLE … ALTER COLUMN … TYPE varchar(2048) USING (col #>> '{}') was
ACCEPTED, and the object was flattened to the literal text
{"url":"https://x/y.png"} in a varchar column. #>> '{}' extracts any json type
as text; "unquotes the JSON string" is only what it does when the cell happens to be a
string.
⇒ An implementer who reads the prescribed statement as delivering the stated abort
silently destroys exactly the rows the backfill has not converted — the rows the
reconciliation exists to find.
The ADR's own confidence gap 2 already says the sketch is unrehearsed. This card is not
that gap; it is the result of the rehearsal it asked for, and the result is that one
half of the prescription does not implement the other half.
What was measured, and what was not
- Measured (live PostgreSQL 16.13, first step-3 rehearsal on that dialect): the
retype above accepts a non-string cell and flattens it. The pre-check that does
implement the ADR's requirement:
select count(*) from t where json_typeof(col) IS DISTINCT FROM 'string' — returned
1 on the same fixture, i.e. it sees the row the DDL did not refuse.
- Measured (SQLite, the same rehearsal):
json_extract(col,'$') gated on
json_valid + json_type = 'text' converts a quoted cell, leaves an already-bare
cell untouched, and is idempotent on re-run. json_type of an un-backfilled
inline-object cell is 'object', so that is the correct abort discriminator there.
- ⚠️ NOT measured: MySQL. Its ordering question (gap 2's own "the retype must precede
the unquote or the two must be one statement") is untouched by this card.
- ⚠️ NOT measured: whether some other Postgres realisation would abort on its own.
#>> '{}' is the idiom the ADR's words describe, and it does not; whether a different
USING expression could is an open question, not a finding.
What this card changes
docs/adr/0104-field-runtime-value-shape-contract.md only — the Confidence gaps
section (and, if the author judges it clearer, the sketch paragraph itself), so that the
governed document no longer reads as though the USING clause performs the abort.
The requirement ("aborting on the first cell that is not a JSON string") is correct
and stays exactly as written; it is the statement list under it that was optimistic.
Two things the wording must end up saying, however it is phrased:
- The abort is a pre-check step 3 runs before any DDL, not a property of the retype.
- The Postgres rehearsal has now happened and this is what it returned — gap 2 is
partly closed by a measurement whose result was negative, and gap 1's Postgres half
moved from "reasoned" to "measured" for this one statement.
Boundaries
- ⛔
docs/adr/** is a governed surface: docs-only PR, draft, human merge. No code
rides along.
- ⛔ Do not fold this into a code PR. The reason this is a separate card at all is
that a code PR may not edit docs/adr/**.
- ⛔ Do not re-open the ADR's decision. D1/D3, the window, and the end-state column are
ruled; this card corrects a statement about how a step is performed, nothing else.
- ⛔ Do not edit
content/docs/releases/**.
Provenance
Measured by the #15989 execution round, which deliberately landed no code and stood up
a live PostgreSQL 16.13 to rehearse the sketch. Ruling and the full measurement:
#15989 (comment)
Part-of #15989.
Generated by Claude Code
What is wrong
ADR-0104's
--applyR4 block promises that step 3 moves a media column whileand the paragraph immediately below prescribes the realisation:
Measured on live PostgreSQL 16.13, those two sentences contradict each other. The
USINGclause does not abort — it converts. With one un-backfilled inline-object rowpresent,
ALTER TABLE … ALTER COLUMN … TYPE varchar(2048) USING (col #>> '{}')wasACCEPTED, and the object was flattened to the literal text
{"url":"https://x/y.png"}in a varchar column.#>> '{}'extracts any json typeas text; "unquotes the JSON string" is only what it does when the cell happens to be a
string.
⇒ An implementer who reads the prescribed statement as delivering the stated abort
silently destroys exactly the rows the backfill has not converted — the rows the
reconciliation exists to find.
The ADR's own confidence gap 2 already says the sketch is unrehearsed. This card is not
that gap; it is the result of the rehearsal it asked for, and the result is that one
half of the prescription does not implement the other half.
What was measured, and what was not
retype above accepts a non-string cell and flattens it. The pre-check that does
implement the ADR's requirement:
select count(*) from t where json_typeof(col) IS DISTINCT FROM 'string'— returned1 on the same fixture, i.e. it sees the row the DDL did not refuse.
json_extract(col,'$')gated onjson_valid+json_type = 'text'converts a quoted cell, leaves an already-barecell untouched, and is idempotent on re-run.
json_typeof an un-backfilledinline-object cell is
'object', so that is the correct abort discriminator there.the unquote or the two must be one statement") is untouched by this card.
#>> '{}'is the idiom the ADR's words describe, and it does not; whether a differentUSINGexpression could is an open question, not a finding.What this card changes
docs/adr/0104-field-runtime-value-shape-contract.mdonly — the Confidence gapssection (and, if the author judges it clearer, the sketch paragraph itself), so that the
governed document no longer reads as though the
USINGclause performs the abort.The requirement ("aborting on the first cell that is not a JSON string") is correct
and stays exactly as written; it is the statement list under it that was optimistic.
Two things the wording must end up saying, however it is phrased:
partly closed by a measurement whose result was negative, and gap 1's Postgres half
moved from "reasoned" to "measured" for this one statement.
Boundaries
docs/adr/**is a governed surface: docs-only PR, draft, human merge. No coderides along.
that a code PR may not edit
docs/adr/**.ruled; this card corrects a statement about how a step is performed, nothing else.
content/docs/releases/**.Provenance
Measured by the
#15989execution round, which deliberately landed no code and stood upa live PostgreSQL 16.13 to rehearse the sketch. Ruling and the full measurement:
#15989 (comment)
Part-of #15989.
Generated by Claude Code