Skip to content
Astera Technology

Why did rerunning a migration multiply already converted values again?

Use the recorded source representation state to convert each old-form row once.

Download complete case notes and answer keys ↓

Read the source and its declared rule

This original transition scales size by 10 from version old to new. Each actual row stores that representation state. A second unguarded UPDATE changes 2→20→200; corrected transactional UPDATE selects old only and marks it new. Unknown or missing versions hold the run before mutation.

Filled synthetic input
RecordInput JSON
Base input{"runs":2,"initial":[{"id":"A","size":2,"version":"old"}],"later":[]}
Download editable CSV ↓

Compare the actual paths

Correct repeated result remains 20. A later old-form rowB3 converts to 30 while earlier A20 stays 20; run count alone is not the row’s conversion state.

Misleading and corrected outputs
PathOutput JSON
Misleading path{"error":null,"records":[{"id":"A","size":200,"version":"new"}]}
Corrected path{"error":null,"records":[{"id":"A","size":20,"version":"new"}]}
Download editable CSV ↓
Local Node.js program: 22.23.3 or 24.14.1
import {DatabaseSync} from 'node:sqlite';
import {TextDecoder} from 'node:util';
import {inflateSync} from 'node:zlib';
import {posix} from 'node:path';
import {createHash} from 'node:crypto';
const rows=(db,sql)=>db.prepare(sql).all().map(r=>({...r}));
const failed=(action)=>{try{action();return null;}catch(e){return {code:e.errcode??e.code??e.name};}};

import assert from 'node:assert/strict';
const run=function run(i,corrected=true){
    const db=new DatabaseSync(':memory:');try{db.exec('CREATE TABLE target(id TEXT PRIMARY KEY,size INTEGER,version TEXT);');
      const add=r=>db.prepare('INSERT INTO target VALUES(?,?,?)').run(r.id,r.size,r.version);for(const r of i.initial)add(r);let error=null;
      for(let n=0;n<i.runs;n++){db.exec('BEGIN;');if(corrected&&db.prepare("SELECT COUNT(*) AS n FROM target WHERE version IS NULL OR version NOT IN('old','new')").get().n){error='incompatible-version';db.exec('ROLLBACK;');break;}
        db.exec("UPDATE target SET size=size*10,version='new'"+(corrected?" WHERE version='old'":''));db.exec('COMMIT;');if(n===0)for(const r of i.later)add(r);}
      return {error,records:rows(db,'SELECT * FROM target ORDER BY id')};
    }finally{db.close();}
  };
const cases=[{"label":"misleading base","input":{"runs":2,"initial":[{"id":"A","size":2,"version":"old"}],"later":[]},"expected":{"error":null,"records":[{"id":"A","size":200,"version":"new"}]},"corrected":false},{"label":"corrected base","input":{"runs":2,"initial":[{"id":"A","size":2,"version":"old"}],"later":[]},"expected":{"error":null,"records":[{"id":"A","size":20,"version":"new"}]},"corrected":true},{"label":"later old-version row converts once","input":{"runs":2,"initial":[{"id":"A","size":2,"version":"old"}],"later":[{"id":"B","size":3,"version":"old"}]},"expected":{"error":null,"records":[{"id":"A","size":20,"version":"new"},{"id":"B","size":30,"version":"new"}]},"corrected":true},{"label":"incompatible source version held","input":{"runs":1,"initial":[{"id":"A","size":2,"version":"other"}],"later":[]},"expected":{"error":"incompatible-version","records":[{"id":"A","size":2,"version":"other"}]},"corrected":true},{"label":"missing representation version held","input":{"runs":1,"initial":[{"id":"A","size":2,"version":null}],"later":[]},"expected":{"error":"incompatible-version","records":[{"id":"A","size":2,"version":null}]},"corrected":true}];
for(const test of cases){
  const actual=run(test.input,test.corrected);
  assert.deepEqual(actual,test.expected,test.label);
  console.log(test.label,actual);
}
Download runnable example ↓

Check boundaries before the next load

A later old row converts once. Incompatible version other and missing version NULL both retain their original 2 and are held.

Editable boundary records and expected outputs
CaseInput JSONExpected output JSON
New old-form row arrives later{"runs":2,"initial":[{"id":"A","size":2,"version":"old"}],"later":[{"id":"B","size":3,"version":"old"}]}{"error":null,"records":[{"id":"A","size":20,"version":"new"},{"id":"B","size":30,"version":"new"}]}
Unknown representation version{"runs":1,"initial":[{"id":"A","size":2,"version":"other"}],"later":[]}{"error":"incompatible-version","records":[{"id":"A","size":2,"version":"other"}]}
Missing representation version{"runs":1,"initial":[{"id":"A","size":2,"version":null}],"later":[]}{"error":"incompatible-version","records":[{"id":"A","size":2,"version":null}]}
Download editable CSV ↓

What size should originalA2 have after two accepted runs?

Show the worked answer

20, not 200. Its first conversion recorded new, so the second pass must leave it unchanged.

Related resources