Skip to content

Qirava DMS docs

QQL batch: concurrent statements

The batch endpoint runs many QQL statements in one HTTP request, concurrently. POST a JSON array of strings to /api/qql/batch; the worker fans every statement out to the executor pool (submit all, wait all), so N queries cost one socket round-trip and run in parallel. Authentication runs once for the whole request, but each statement is authorized individually. The response is a JSON array of per-statement envelopes in submission order. An optional atomic mode pre-authorizes every statement so one denial rejects the whole batch. This page covers the request format, concurrency model, authorization, and response shape.

Request format#

POST a JSON array of QQL strings to /api/qql/batch. The body must be a non-empty JSON array of strings (the parser handles the \" \\ \n \t \r \/ escapes a query body uses); a non-array or malformed body is a bad_request.

Request

curl -X POST http://localhost:8080/api/qql/batch \
  --data '["SELECT * FROM users", "SELECT count(*) FROM users"]'

Response

[
  { "error": null, "data": [ { "_id": 1, "name": "alice" } ], "root": { "count": 1 } },
  { "error": null, "data": [ { "count_all": 1 } ], "root": { "count": 1 } }
]

Concurrent execution#

Each statement is keyed exactly as a single /api/qql call is keyed: SELECT and INSERT run concurrently; UPDATE/DELETE on the same record still serialize on their table+id key. Statements are submitted to the pool first and then collected, so they run in parallel on the executor. A bounded sliding window (scaled to the worker pool, clamped to 64–8192 in flight) lets an arbitrarily large batch process fully and concurrently without overrunning the queue. Results are kept in submission order.

  ["q1", "q2", ... "qN"]
        │ submit all (within sliding window)
        ▼
   executor pool runs concurrently (per-record writes still serialize)
        │ collect in submission order
        ▼
   [ env1, env2, ... envN ]
Batch fan-out

Per-statement authorization#

When the route is covered by an auth group, the request is authenticated ONCE (the same key for the whole request); an invalid key rejects the entire request. Then each statement is authorized individually against the key's scope and per-table grants (the L3 planner gate). A statement that is denied gets its OWN access_denied envelope in the array while the others still run.

Atomic (all-or-nothing) mode#

Add ?atomic=true (or ?mode=all) to pre-authorize EVERY statement before running any: if one statement is unauthorized, the whole batch is rejected with a 403 (deny-before-execute) and nothing runs. In atomic mode, if any statement fails at runtime the overall HTTP status is 422 (the array still shows which failed). Default (non-atomic) mode always returns 200, with each envelope carrying its own outcome.

Response envelope array#

The response is a JSON array with one element per input statement, in the same order. Each element is a full response envelope ({ error, data, root }) — successes and per-statement access_denied errors are interleaved as they occur.