Read the source and its declared rule
The original batch allows independent document families to complete separately. D contains a header, L1=2, L2=−1; E has its own valid line 3. Each existing line requires a non-NULL positive quantity. D’s header and lines must succeed together. A savepoint per family removes all D allocations on its line error; header-only drafts are explicitly allowed.
| Record | Input JSON |
|---|---|
| Base input | {"documents":[{"id":"D","lines":[{"item":"L1","quantity":2},{"item":"L2","quantity":-1}]},{"id":"E","lines":[{"item":"L1","quantity":3}]}]} |
Compare the actual paths
Corrected target retains E and its line, with D’s code 275 recorded and no D fragments. Row-by-row catching instead leaves D header and L1.
| Path | Output JSON |
|---|---|
| Misleading path | {"errors":[{"document":"D","code":275}],"documents":[{"id":"D"},{"id":"E"}],"lines":[{"document_id":"D","item":"L1","quantity":2},{"document_id":"E","item":"L1","quantity":3}]} |
| Corrected path | {"errors":[{"document":"D","code":275}],"documents":[{"id":"E"}],"lines":[{"document_id":"E","item":"L1","quantity":3}]} |
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('PRAGMA foreign_keys=ON;CREATE TABLE document(id TEXT PRIMARY KEY);CREATE TABLE line(document_id TEXT REFERENCES document(id),item TEXT,quantity INTEGER NOT NULL CHECK(quantity>0));');const errors=[];
for(const d of i.documents){if(corrected)db.exec('SAVEPOINT family;');db.prepare('INSERT INTO document VALUES(?)').run(d.id);
let error=null;for(const l of d.lines){error=failed(()=>db.prepare('INSERT INTO line VALUES(?,?,?)').run(d.id,l.item,l.quantity));if(error){errors.push({document:d.id,code:error.code});if(corrected)break;}}
if(corrected){if(error)db.exec('ROLLBACK TO family;');db.exec('RELEASE family;');}}
return {errors,documents:rows(db,'SELECT * FROM document ORDER BY id'),lines:rows(db,'SELECT * FROM line ORDER BY document_id,item')};
}finally{db.close();}
};
const cases=[{"label":"misleading base","input":{"documents":[{"id":"D","lines":[{"item":"L1","quantity":2},{"item":"L2","quantity":-1}]},{"id":"E","lines":[{"item":"L1","quantity":3}]}]},"expected":{"errors":[{"document":"D","code":275}],"documents":[{"id":"D"},{"id":"E"}],"lines":[{"document_id":"D","item":"L1","quantity":2},{"document_id":"E","item":"L1","quantity":3}]},"corrected":false},{"label":"corrected base","input":{"documents":[{"id":"D","lines":[{"item":"L1","quantity":2},{"item":"L2","quantity":-1}]},{"id":"E","lines":[{"item":"L1","quantity":3}]}]},"expected":{"errors":[{"document":"D","code":275}],"documents":[{"id":"E"}],"lines":[{"document_id":"E","item":"L1","quantity":3}]},"corrected":true},{"label":"complete entity commits","input":{"documents":[{"id":"D","lines":[{"item":"L1","quantity":2},{"item":"L2","quantity":1}]}]},"expected":{"errors":[],"documents":[{"id":"D"}],"lines":[{"document_id":"D","item":"L1","quantity":2},{"document_id":"D","item":"L2","quantity":1}]},"corrected":true},{"label":"permitted header-only draft","input":{"documents":[{"id":"D","lines":[]}]},"expected":{"errors":[],"documents":[{"id":"D"}],"lines":[]},"corrected":true},{"label":"missing line quantity holds its complete family","input":{"documents":[{"id":"D","lines":[{"item":"L1","quantity":2},{"item":"L2","quantity":null}]},{"id":"E","lines":[{"item":"L1","quantity":3}]}]},"expected":{"errors":[{"document":"D","code":1299}],"documents":[{"id":"E"}],"lines":[{"document_id":"E","item":"L1","quantity":3}]},"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);
}
Check boundaries before the next load
A fully valid D commits. A permitted header-only D keeps its header and no lines. If an existing D line lacks quantity, NOT NULL1299 rolls back D while independent valid E remains.
| Case | Input JSON | Expected output JSON |
|---|---|---|
| All lines in one family valid | {"documents":[{"id":"D","lines":[{"item":"L1","quantity":2},{"item":"L2","quantity":1}]}]} | {"errors":[],"documents":[{"id":"D"}],"lines":[{"document_id":"D","item":"L1","quantity":2},{"document_id":"D","item":"L2","quantity":1}]} |
| Allowed header-only draft | {"documents":[{"id":"D","lines":[]}]} | {"errors":[],"documents":[{"id":"D"}],"lines":[]} |
| Missing line quantity holds its family | {"documents":[{"id":"D","lines":[{"item":"L1","quantity":2},{"item":"L2","quantity":null}]},{"id":"E","lines":[{"item":"L1","quantity":3}]}]} | {"errors":[{"document":"D","code":1299}],"documents":[{"id":"E"}],"lines":[{"document_id":"E","item":"L1","quantity":3}]} |
Must E roll back because independent D failed?
Show the worked answer
No under this supplied family policy. Roll back all of D, retain complete E, and record D’s unresolved error.