Brief a Stored-Field Migration Across Old and New Readers

Drafted and reviewed using AI assistance.

On this page

Before asking an agent to rename a stored field, describe the readers and writers that can still use its old name. Then define how existing records move, how conflicting values are resolved, and when returning to the old application stops being a simple code rollback.

A field rename has a longer life than a source-code rename. Old records, background jobs, and readers may outlive the code you are changing. Danilo Sato's Parallel Change describes expanding an interface, moving its consumers, and removing the old interface afterward. The Prisma ORM v7 migration guide provides a concrete column-migration example. Neither source supplies the write-coordination policy for your application. The worked contract below chooses one explicitly.

Start with complete records, including awkward ones

The fictional application stores a nullable name as old_name. Its replacement, display_name, has the same meaning and permitted values: a string, including an empty string, or null. An absent field is a different state from a present field containing null.

These eight records are original teaching data. They represent a mixed snapshot after an interrupted expansion, not a database export or a successful migration. All later states in this guide are manually specified expectations.

[
  {"id":"m1","revision":1,"old_name":"Ari"},
  {"id":"m2","revision":1,"old_name":null},
  {"id":"m3","revision":3,"old_name":"Bo","display_name":"Bo"},
  {"id":"m4","revision":5,"old_name":"Cora","display_name":"Coral"},
  {"id":"m5","revision":2,"old_name":null,"display_name":"Dee"},
  {"id":"m6","revision":4,"display_name":"Eli"},
  {"id":"m7","revision":1,"old_name":""},
  {"id":"m8","revision":2,"old_name":null,"display_name":null}
]

The example legacy reader returns old_name when present and null when absent. A new reader will require display_name to be present. The revision numbers describe the supplied history; they do not prescribe a database concurrency mechanism.

Records Decision before copying
m1, m2, m7 New field absent: eligible for an exact copy once the write pause is established
m3, m8 Both fields already agree: leave values and revisions unchanged
m4 Conflicting strings: stop automatic treatment and obtain an explicit resolution
m5 Present null versus a string: also a conflict; do not interpret null as “missing”
m6 New-only value: the legacy reader would see null; resolve the compatibility gap before declaring the population ready

Copying only the easy records would not make the dataset compatible. In particular, choosing “prefer the new field” as a fallback would silently decide m4 and m5 without a requirement source.

Choose how to stop writers crossing the backfill

Here is a counterexample to an uncoordinated copy:

Event Record values
Copier reads m1 at revision 1 old_name="Ari"; new field absent
Legacy writer W0 updates m1 Revision 2, old_name="Aria"; new field still absent
Copier uses its stale read unconditionally New field becomes "Ari" while old remains "Aria"

The rows are a hypothetical history, not an observed race. They show why “copy old to new” does not establish safe coexistence with an old-only writer.

For the rest of this exercise, choose a write pause for the records being migrated. Reads may continue, but all application, job, administrative, and migration-related writes to those records must be accounted for. Drain or otherwise resolve work already in flight before taking the migration snapshot. If that cannot be established, stop at an investigation plan.

This choice trades write availability for a simpler transition. It does not claim a zero-downtime migration. A real system that must keep writing needs its own supported coordination and conflict design; this guide does not infer safe dual writes from two field assignments.

Resolve conflicts, then fill only absent new fields

For this fictional exercise, a supplied resolution note makes three decisions:

  • D1: m4's intended name is Coral; set both fields to it and move revision 5 to 6.
  • D2: m5 intentionally has no name; retain null in both fields and move revision 2 to 3.
  • D3: m6's intended name is Eli; populate the missing old field for legacy readers and move revision 4 to 5.

These are invented decisions for the example, not permissions to resolve your records in the same direction. Preserve the conflicting before-values and the rationale in your own migration record.

With writes still paused, fill an absent display_name from a present old_name. Copy null and "" exactly. Do not overwrite any existing new value. Stop if a present pair conflicts or a required source is absent.

The expected result is:

ID Revision old_name display_name
m1 2 "Ari" "Ari"
m2 2 null null
m3 3 "Bo" "Bo"
m4 6 "Coral" "Coral"
m5 3 null null
m6 5 "Eli" "Eli"
m7 2 "" ""
m8 2 null null

On a second backfill pass in this same paused phase, no record changes and no revision increments. “New field is absent” is the selection rule; “new field is null” would incorrectly select m2, m5, and m8 again.

Unexpected writes or revisions require a stop and reconciliation. A partial pass is not completion; record which rows changed before deciding how to continue. No automatic rollback or backup-restoration behavior is assumed here.

Write the compatibility table before switching consumers

Phase Record expectation Readers Writers Condition for moving on
Expand Old field remains; new field may be absent Legacy R0 reads old field Pause and account for W0 and every other writer before migration Snapshot and outstanding work are understood
Migrate Resolve D1–D3, then fill absent new fields; all eight pairs agree R0 can still read old values All writes to these records remain paused No unresolved row; repeated pass changes nothing
Switch reads Same complete, equal pairs Move consumers to R1, which reads the new field Still paused Every old reader and old writer is retired or otherwise ruled out
Switch writes New field becomes authoritative R1 only Resume new-only W1; W0 stays disabled New edits and new records follow the new contract
Contract Remove old field after the dependency inventory is satisfied R1 only W1 only No old consumer, report, job, or rollback assumption depends on old data

This plan deliberately keeps writes paused while readers move. It avoids needing a period of dual writes. An unknown old client or background writer is a reason to retain the compatibility phase, not evidence that it is probably unused.

After W1 starts, suppose it makes these two changes:

[
  {"id":"m1","revision":3,"old_name":"Ari","display_name":"Aria"},
  {"id":"m9","revision":1,"display_name":"Faye"}
]

The stale old value on m1 and absent old field on m9 are now expected under the new-only writer policy. A legacy reader would see Ari and null rather than Aria and Faye. That is why it must be gone before W1 resumes.

The old backfill is rerunnable within its defined phase, not indefinitely. Do not restart it after new-only writes begin and call those intentional differences corrupt data.

Name the point where rollback changes meaning

Before new-only writes resume, the example retains equal old and new values, and an old-reader fallback can be considered while writes stay paused. That is still a proposed compatibility route to verify.

After W1 writes, switching only the application binary back would expose stale or absent old values. After the contract phase removes old_name, this example's legacy reader would return null for every record. Reintroducing the field from current new values would be a separate forward repair, requiring a verified mapping and coordination; restoring an older snapshot could also discard subsequent work.

Write those limits into the plan before deleting the compatibility field. “We have a backup” is not evidence that a restore has been checked or that post-migration edits can be reconciled.

Copy the investigation request

Plan the migration of old_name to display_name for the supplied
fictional records. Do not connect to a database or change real data.

Meaning:
Both names represent the same nullable string. Empty string, null,
and absent are distinct. IDs and unrelated fields must be preserved.

Inventory legacy/new readers and writers, including jobs and in-flight
work. The selected policy pauses all writes to the migrated records
before the snapshot and keeps them paused through the read switch.
If that pause cannot be established, return the missing evidence and
a plan; do not describe an uncoordinated copy as safe dual writing.

Use the eight complete input records and explicit D1–D3 resolutions.
Fill only absent display_name fields. Do not overwrite an existing
new field or select records merely because its value is null.

Provide expected records for expand, resolved/backfilled, read switch,
new-only edits/new rows, and old-field removal. Show a second backfill
pass with no changes while writes remain paused.

Old readers and writers must be retired before new-only W1 resumes.
Keep the m1 edit and new m9 example. Explain why the old copier and
old-reader rollback are no longer valid after that point.

Return a bounded plan, per-stage stop conditions, unresolved storage
assumptions, and rollback limits. Separate manual examples from any
actually performed checks. No database, installation, deletion,
production request, release or backup restore is authorized here.

The useful delivery is a transition whose records, consumers, and stopping points agree. A successful field copy is only one part of that contract.