Symptom
POST /api/v1/data/showcase_private_note/batch with no options (so atomic defaults to false on this door) and three records — valid, failing, valid — answers:
200 {
"total": 3,
"succeeded": 1,
"failed": 1,
"results": [ { idx 0: ok }, { idx 1: VALIDATION_FAILED } ]
}
Four things are wrong in one body:
- only 2 results for 3 records;
- no
NOT_ATTEMPTED entry for idx 2 — the caller cannot tell it was skipped;
succeeded + failed (2) != total (3), so the counters do not reconcile;
- the third, valid record returns 0 rows on re-read — it was never attempted, and no amount of reading the response says so.
Expected per ADR-0119 D4 (atomic defaults false on this door): valid rows land, and neither valid row is blocked by the other. Reproduced 2/2.
Two same-run controls isolate it:
| variant |
result |
continueOnError: true |
3 results, both valid rows persisted |
atomic: true |
the correct ROLLED_BACK / VALIDATION_FAILED / NOT_ATTEMPTED triple |
(The rest of the checklist item passes: the discovery bit matches live behaviour incl. $ref parenting and BATCH_UNRESOLVED_REF; atomic:false on the cross-object door is refused 400 BATCH_NOT_ATOMIC; cross-object rollback held at both write-time and validation-time failures; BATCH_TOO_LARGE brackets exactly at 200/201 on all three doors.)
Root cause
The non-atomic loop breaks out of the record loop on the first failure, while buildBatchDataResponse (packages/metadata-protocol/src/protocol.ts) reports total = records.length. The un-attempted tail is therefore invisible twice over: it produces no results[] entry and it is not counted in either bucket, so the arithmetic mismatch is the only trace of it.
Same family, recorded in the run's smaller observations: per-object bulk counters under-report whenever a row fails without continueOnError — worth fixing in the same pass. Related but distinct: #4620 (closed/completed) covered deleteManyData / updateManyData atomicity, not this loop's break-and-miscount.
Reproduction
- Boot showcase on a fresh isolated file DB (
SqlDriver / better-sqlite3); authenticate as admin@objectos.ai.
POST /api/v1/data/showcase_private_note/batch with no options key and three records: valid, one that fails validation, valid.
- Observe
200 {total:3, succeeded:1, failed:1, results:[idx0 ok, idx1 VALIDATION_FAILED]} — no idx 2 entry.
- Re-read the object: the third record is absent (0 rows).
- Controls: repeat with
options.continueOnError = true → 3 results, both valid rows persisted; repeat with options.atomic = true → ROLLED_BACK / VALIDATION_FAILED / NOT_ATTEMPTED.
Source
Extracted from the QA run #7463 (framework a86db17).
Symptom
POST /api/v1/data/showcase_private_note/batchwith nooptions(soatomicdefaults tofalseon this door) and three records — valid, failing, valid — answers:Four things are wrong in one body:
NOT_ATTEMPTEDentry for idx 2 — the caller cannot tell it was skipped;succeeded + failed(2) !=total(3), so the counters do not reconcile;Expected per ADR-0119 D4 (
atomicdefaults false on this door): valid rows land, and neither valid row is blocked by the other. Reproduced 2/2.Two same-run controls isolate it:
continueOnError: trueatomic: trueROLLED_BACK/VALIDATION_FAILED/NOT_ATTEMPTEDtriple(The rest of the checklist item passes: the discovery bit matches live behaviour incl.
$refparenting andBATCH_UNRESOLVED_REF;atomic:falseon the cross-object door is refused400 BATCH_NOT_ATOMIC; cross-object rollback held at both write-time and validation-time failures;BATCH_TOO_LARGEbrackets exactly at 200/201 on all three doors.)Root cause
The non-atomic loop breaks out of the record loop on the first failure, while
buildBatchDataResponse(packages/metadata-protocol/src/protocol.ts) reportstotal = records.length. The un-attempted tail is therefore invisible twice over: it produces noresults[]entry and it is not counted in either bucket, so the arithmetic mismatch is the only trace of it.Same family, recorded in the run's smaller observations: per-object bulk counters under-report whenever a row fails without
continueOnError— worth fixing in the same pass. Related but distinct: #4620 (closed/completed) covereddeleteManyData/updateManyDataatomicity, not this loop's break-and-miscount.Reproduction
SqlDriver/ better-sqlite3); authenticate asadmin@objectos.ai.POST /api/v1/data/showcase_private_note/batchwith nooptionskey and three records: valid, one that fails validation, valid.200 {total:3, succeeded:1, failed:1, results:[idx0 ok, idx1 VALIDATION_FAILED]}— no idx 2 entry.options.continueOnError = true→ 3 results, both valid rows persisted; repeat withoptions.atomic = true→ROLLED_BACK/VALIDATION_FAILED/NOT_ATTEMPTED.Source
Extracted from the QA run #7463 (framework a86db17).