Skip to content

Commit 7f79c8b

Browse files
committed
docs(api): describe the stopped-batch NOT_ATTEMPTED tail on the non-atomic arm (#7539)
`data-api.mdx` described `atomic: false` as "sequential best-effort, stopping at the first failure" without saying what the response contains for the records it never reached — the shape the fix makes explicit. `batch.mdx` is regenerated from the two `.describe()` strings this change touches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016gd2bypaK4KYs78q8RP38G
1 parent 9df3743 commit 7f79c8b

2 files changed

Lines changed: 6 additions & 4 deletions

File tree

content/docs/api/data-api.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ Execute a batch operation (create / update / upsert / delete) on multiple record
241241

242242
**Response**: `BatchUpdateResponse` with `succeeded`, `failed`, `total`, and a per-record `results` array. Each entry in `results` has `id`, `success`, `index` (the row's position in the request array), an optional `errors` array (`ApiError[]` — read `errors[0].message`, branch on `errors[0].code`), and optional `data` (the full record, present when `returnRecords` is `true`).
243243

244-
`options.atomic` defaults to `false` (sequential best-effort, stopping at the first failure). Set it to `true` and the whole batch runs inside one transaction: the first failure rolls back every prior write, and the response reports `succeeded: 0` — each row's `errors[0].code` says what happened: `ROLLED_BACK` (written, then undone), the causal row's own error code, or `NOT_ATTEMPTED` (never reached). A deployment whose driver cannot roll back rejects an atomic request with `501 NOT_IMPLEMENTED` instead of running it best-effort — probe `capabilities.transactionalBatch` on `/discovery` first. `atomic` takes precedence over `continueOnError`.
244+
`options.atomic` defaults to `false`: sequential best-effort that stops at the first failure. Records written before the failure stay written — nothing is rolled back on this arm — and every record after it is reported with `errors[0].code` `NOT_ATTEMPTED` rather than omitted, so `results` always covers all `total` records and `succeeded + failed === total` (#7539). Send `continueOnError: true` to process the remaining records instead of stopping. Set `atomic` to `true` and the whole batch runs inside one transaction: the first failure rolls back every prior write, and the response reports `succeeded: 0` — each row's `errors[0].code` says what happened: `ROLLED_BACK` (written, then undone), the causal row's own error code, or `NOT_ATTEMPTED` (never reached). A deployment whose driver cannot roll back rejects an atomic request with `501 NOT_IMPLEMENTED` instead of running it best-effort — probe `capabilities.transactionalBatch` on `/discovery` first. `atomic` takes precedence over `continueOnError`.
245245

246246
### `POST /data/:object/createMany`
247247

@@ -284,7 +284,9 @@ selects rows, so no body key can widen the delete into a filter.
284284
deleted one at a time by primary key, so each honours `deleteBehavior`
285285
(`cascade` / `set_null` / `restrict`) on relations pointing at it. The run stops
286286
at the first failure; `continueOnError: true` processes the remaining ids and
287-
reports the failures instead.
287+
reports the failures instead. Either way every id gets a `results` entry — the
288+
ids a stopped run never reached carry `errors[0].code` `NOT_ATTEMPTED`, so the
289+
counters reconcile against `total` (#7539).
288290

289291
`options.atomic: true` is honoured here the same way as on `/batch` (#4620): the
290292
whole id list runs inside one transaction, the first failure rolls back every

content/docs/references/api/batch.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const result = BatchConfigSchema.parse(data);
5555
| :--- | :--- | :--- | :--- |
5656
| **id** | `string` | optional | Record ID if operation succeeded |
5757
| **success** | `boolean` || Whether this record was processed successfully |
58-
| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }[]` | optional | Array of errors if operation failed. Branch on `errors[0].code` — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error (#4793). |
58+
| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +259 more>; message: string; category?: string; httpStatus?: integer; … }[]` | optional | Array of errors if operation failed. Branch on `errors[0].code` — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error (#4793). A NON-atomic batch that stopped (the `continueOnError: false` default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back (#7539). |
5959
| **data** | `Record<string, any>` | optional | Full record data (if returnRecords=true) |
6060
| **index** | `number` | optional | Index of the record in the request array |
6161
| **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]` | optional | Write-observability (#3407/#3431/#3455): caller-supplied fields LEGALLY stripped from THIS row before it was written — static `readonly` (#2948) / TRUE `readonlyWhen` (#3042) on update, or the #3043 create-ingress strip. Per-row because a batch can drop different fields on different rows (`readonlyWhen` is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit `X-ObjectStack-Dropped-Fields` for batches. Optional — omit-when-empty keeps the shape backward-compatible. |
@@ -83,7 +83,7 @@ const result = BatchConfigSchema.parse(data);
8383
| :--- | :--- | :--- | :--- |
8484
| **atomic** | `boolean` || Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries `errors[0].code` ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe `capabilities.transactionalBatch` on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort. |
8585
| **returnRecords** | `boolean` || If true, return full record data in response |
86-
| **continueOnError** | `boolean` || If true (and atomic=false), continue processing remaining records after errors |
86+
| **continueOnError** | `boolean` || If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported `errors[0].code` NOT_ATTEMPTED rather than omitted, so `results` always covers all `total` records and `succeeded + failed === total` (#7539). |
8787
| **validateOnly** | `never` | optional | [REMOVED] `options.validateOnly` was removed from BatchOptions in @objectstack/spec (#4052). It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds. |
8888

8989

0 commit comments

Comments
 (0)