Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,20 @@ HTTP status: `200`.

- `results` (REQUIRED, array of single-statement result objects) — in the same order as the request `batch`. Each element has the shape of section 6.1.

If an atomic batch fails partway through, the response is the error envelope in section 7, not a partial `results` array.
A `results` array MUST have the same length as the request `batch`. A server MUST NOT return status `200` with a `results` array covering only some of the submitted statements.

#### 6.2.1 Batch failure

If a statement in a batch fails, the response is the error envelope in section 7, not a partial `results` array. This holds for both atomic and non-atomic batches.

Servers MUST execute the statements of a non-atomic batch one at a time, in `batch` array order, and MUST stop at the first statement that fails. Without this, `error.statementIndex` does not partition the batch: a server that executed statements concurrently could commit a later statement while an earlier one failed, and the client could conclude nothing about what persisted. Atomic batches carry no ordering obligation, because the transaction makes execution order unobservable.

Given that, the two cases differ in what persists, and the difference is normative:

- **Atomic batch** (`atomic: true`) — the transaction rolls back. No statement in the batch has any effect.
- **Non-atomic batch** (`atomic` absent or `false`) — statements preceding the failing statement have been executed and their effects persist. The failing statement and all statements after it have not been executed. Clients MUST NOT treat a non-atomic batch error as no-statements-applied.

For a non-atomic batch failure the server MUST include `error.statementIndex` (section 7), because it is the only means by which a client can determine how far the batch got. No such obligation applies to an atomic batch failure: nothing persisted, so there is nothing for the client to locate.

## 7. Error responses

Expand All @@ -199,7 +212,7 @@ HTTP status: `4xx` or `5xx`.

- `error.code` (REQUIRED, string) — one of the registered codes below, or a vendor-namespaced code (`vendor:<name>`).
- `error.message` (REQUIRED, string) — human-readable explanation. Servers SHOULD avoid leaking sensitive details.
- `error.statementIndex` (OPTIONAL, integer) — for batch requests, the zero-based index of the statement that failed. Omitted for single-statement requests.
- `error.statementIndex` (REQUIRED for non-atomic batch statement failures, otherwise OPTIONAL, integer) — the zero-based index of the statement that failed. For a non-atomic batch failure it is the client's only means of determining which statements persisted (section 6.2.1), so it MUST be present. MUST be omitted for single-statement requests.

Registered error codes in v0.1:

Expand Down Expand Up @@ -252,8 +265,10 @@ A v0.1 conforming server MUST:
3. Return the success envelopes defined in section 6 for successful execution.
4. Return the error envelope defined in section 7 for any failure, using the HTTP status codes in the table.
5. Honor `atomic: true` on batch requests.
6. Accept the registered parameter types in section 5 (`blob`, `bigint`).
7. Emit the `X-Http-Sql-Version` response header.
6. Execute a non-atomic batch sequentially in array order, stopping at the first failure (section 6.2.1).
7. On a batch statement failure, return the error envelope rather than a partial `results` array, and include `error.statementIndex` when the batch was non-atomic (section 6.2.1).
8. Accept the registered parameter types in section 5 (`blob`, `bigint`).
9. Emit the `X-Http-Sql-Version` response header.

A v0.1 conforming server MAY:

Expand All @@ -268,7 +283,8 @@ A v0.1 conforming client MUST:
2. Send exactly one of `sql` or `batch` in the request body.
3. Use the standard parameter encoding from section 5.
4. Handle the success and error envelopes from sections 6 and 7.
5. Not require any vendor-specific request or response fields beyond those defined here.
5. On a non-atomic batch error, treat the statements preceding `error.statementIndex` as applied (section 6.2.1). A client MUST NOT assume no statements were applied.
6. Not require any vendor-specific request or response fields beyond those defined here.

A v0.1 conforming client SHOULD:

Expand Down
5 changes: 3 additions & 2 deletions conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ Conformance is self-asserted. The community can call out failures via issues.
|-------|------------------------------------------------------------------|---------------------------------------------|
| B-1 | Two INSERTs, no `atomic` | 200, `results` array length 2 |
| B-2 | Two INSERTs with `atomic: true` | 200, `results` array length 2 |
| B-3 | Atomic batch where the second statement fails | 400, error envelope, no rows persisted |
| B-4 | Non-atomic batch where the second statement fails | 400, error envelope (servers MAY also return 200 with partial results -- the spec leaves this implementation-defined; recommended behavior is to fail closed) |
| B-3 | Atomic batch where the second statement fails | 400, error envelope. NO statement persisted -- the first INSERT is absent. |
| B-4 | Non-atomic batch where the second statement fails | 400, error envelope, `error.statementIndex` = 1. The FIRST statement PERSISTS -- its INSERT is present. |
| B-5 | Non-atomic three-statement batch where the second fails | 400, error envelope, `error.statementIndex` = 1. First statement persists, THIRD did not execute. |

### Parameter types

Expand Down
19 changes: 16 additions & 3 deletions examples/reference-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ export class HttpSqlClient {
}

async batch(statements: Statement[], atomic = false): Promise<BatchResult> {
return this.send({ batch: statements, atomic }) as Promise<BatchResult>;
return this.send({ batch: statements, atomic }, atomic) as Promise<BatchResult>;
}

private async send(body: unknown): Promise<unknown> {
// `atomic` is undefined for single-statement requests.
private async send(body: unknown, atomic?: boolean): Promise<unknown> {
const res = await fetch(this.endpoint, {
method: "POST",
headers: {
Expand All @@ -48,7 +49,19 @@ export class HttpSqlClient {
code: "internal_error",
message: `HTTP ${res.status}`,
};
throw Object.assign(new Error(err.message), { code: err.code, statementIndex: err.statementIndex });
// Section 6.2.1: a non-atomic batch error does NOT mean nothing applied --
// statements before statementIndex persisted. Atomic batches rolled back,
// and a failed single statement applied nothing, so both are 0.
// `undefined` means exactly one thing: a non-atomic batch whose server
// omitted the REQUIRED statementIndex, so how far it got is UNKNOWN.
// Never treat that as zero -- a caller reading zero replays applied work.
const appliedCount = atomic === false ? err.statementIndex : 0;

throw Object.assign(new Error(err.message), {
code: err.code,
statementIndex: err.statementIndex,
appliedCount,
});
}

return json;
Expand Down
21 changes: 18 additions & 3 deletions examples/reference-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export async function handle(req: Request, auth: (req: Request) => boolean): Pro
return ok({ results });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return errorResponse(400, "sql_error", message);
const statementIndex = (err as { statementIndex?: number }).statementIndex;
return errorResponse(400, "sql_error", message, statementIndex);
}
}

Expand All @@ -61,8 +62,22 @@ async function execute(_stmt: Statement): Promise<Result> {
return { columns: [], rows: [], rowsAffected: 0, lastInsertId: null };
}

async function executeBatch(batch: Statement[], _atomic: boolean): Promise<Result[]> {
return Promise.all(batch.map((s) => execute(s)));
async function executeBatch(batch: Statement[], atomic: boolean): Promise<Result[]> {
// SPEC.md 6.2.1: batches execute sequentially in array order, stopping at
// the first failure; a non-atomic failure carries error.statementIndex.
// In a real server the atomic branch wraps this loop in a transaction.
const out: Result[] = [];
for (let i = 0; i < batch.length; i++) {
try {
out.push(await execute(batch[i]));
} catch (err) {
if (!atomic && err !== null && typeof err === "object") {
(err as { statementIndex?: number }).statementIndex = i;
}
throw err;
}
}
return out;
}

function ok(body: unknown): Response {
Expand Down
Loading