Skip to content
About
Main content

Evolution operations and expand/contract

Which operations are safe, and why does a rename take six steps?

The table of safe operations is not a convention. It follows from one criterion:

("Information-preserving map" means: every old value is converted to a new value, and no two different old values become the same. This is total and from "Before you start" on the overview.)

OperationSafe?Why
Add with defaultBoth directionsAn value maps to the default in a fixed way; old readers ignore the new field
Add NeverOld data has no value for it, and "no default" is by definition "no mapping"
Drop optional fieldBackward onlyOld readers may still reference the field
Widen a typeYesInjective and total over the old domain
Narrow a typeNever as a schema changeNot total: some old values do not fit. It is a data migration wearing a schema edit's costume
ReorderFree under name or ID identity; fatal under positionSee Identity by name and by ID
RenameEntirely decided by the ID: only. Name: drop plus add. Position: no effect

(In some formats a dropped field that has a default is safe in both directions, as in the A to B step above. The table row describes the general case: if an old reader has no default for the field, it breaks.)

Why rename needs several steps

Under name identity, rename is "drop plus add". Drop breaks readers. So the only safe rename is the protocol (also called parallel change):

  1. Expand: add the new field (weight_kg) next to the old one (weight).
  2. Dual-write: write both fields in every new record.
  3. Backfill: fill the new field for old rows.
  4. Migrate readers: move every reader to the new field.
  5. Verify: prove that nobody reads the old field.
  6. Contract: drop the old field.

Treating it as one operation is the exact mistake Ravi made. The procedure exists to correct that mistake.

The hard step is the last one

Dropping requires proving nobody reads the . Proof requires read telemetry: query-log mining or column-level . Organizations without it never complete the . That is why mature tables collect dozens of dead columns. It is not laziness. It is the absence of the observation mechanism that would make the last step decidable. It is the cleanest example here of a technical decision blocked by a missing source of information, not a missing feature.

expand/contract for weight -> weight_kg (timeline)

week 0   schema: weight                     writers: weight
week 1   schema: weight, weight_kg          writers: weight AND weight_kg   (expand + dual-write)
week 2   backfill old rows: weight_kg = weight
week 3-6 readers move one by one to weight_kg
week 7   query log shows no read of `weight` for 30 days  (verify)
week 8   schema: weight_kg                  (contract / drop)

Safe change means an injective map over existing data. Rename is expand/contract, and you can only finish the if you can observe reads.