How LuraDB works
Consistency and conditional writes
applies to v0.5.1Keys and documents carry an ETag. If-Match makes a write conditional on the version you read, and If-None-Match: * creates only when nothing exists yet. A domain created with require_conditional_writes rejects unconditional single-key writes with 428.
Every key and document has an ETag
Section titled “Every key and document has an ETag”A read of a key or a document answers with an ETag header that names the version you got:
curl -si http://127.0.0.1:3000/store-api/kv/default/keys/greeting \ -H "Authorization: Bearer $KEY"HTTP/1.1 200 OKcontent-type: application/octet-streametag: "1a1a35bd23a0000"content-length: 5
helloA document answers the same way, "8c14f773263a725c-1" being its tag at _version 1. Treat an ETag as an opaque string and send it back unchanged. Every write gives the key or document a new one, and a key or document that is deleted and written again never gets a tag it had before, so a stale tag cannot match by accident. A key in the explicit null state has an ETag too: its read answers 204 with the header.
If-Match writes only if nothing changed since you read
Section titled “If-Match writes only if nothing changed since you read”Send the tag of the version you read in If-Match. If another writer got in between, the write is refused with 412 Precondition Failed, nothing is written, and the answer carries the tag of the current version:
ETAG='"1a1a35bd23a0000"'curl -si -X PUT http://127.0.0.1:3000/store-api/kv/default/keys/greeting \ -H "Authorization: Bearer $KEY" -H "If-Match: $ETAG" -d 'hello again'HTTP/1.1 200 OKThe same request again, now with a stale tag, loses:
HTTP/1.1 412 Precondition Failedetag: "1a1a35bd2500000"
412 Precondition Failed: the key's current version does not satisfy the write preconditionA client that wants to merge reads the key again and retries with the new tag. The check and the write are one step: they run while the key’s write lock is held, so no other single-key write of the same domain can fall between them.
If-Matchon a key or document that does not exist answers412without anETag, because there is no current version to name.- A value that is not a tag, such as
If-Match: nonsense, answers400 Bad Request. PUT,DELETEandPATCH …/nullon a key take the header, and so doPUTandDELETEon a JSON document.
For a JSON document the refusal reads differently:
curl -si -X PUT http://127.0.0.1:3000/store-api/json/orders/documents/o-1001 \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -H 'If-Match: "8c14f773263a725c-1"' -d '{"status": "cancelled", "total": 42.5}'HTTP/1.1 412 Precondition Failedetag: "8c14f773263a725c-2"
version conflict: expected "8c14f773263a725c-1", actual "8c14f773263a725c-2"Releases before 0.5.1 answered 409 here; see Compatibility.
If-None-Match: * creates only
Section titled “If-None-Match: * creates only”If-None-Match: * writes only when the key or document does not exist. The first request creates, the second finds the key and is refused with 412 and the current tag:
curl -si -X PUT http://127.0.0.1:3000/store-api/kv/default/keys/fresh \ -H "Authorization: Bearer $KEY" -H "If-None-Match: *" -d 'first'HTTP/1.1 200 OKThe literal * is the only value accepted; anything else answers 400. Use it to claim a name, a lock or an idempotency key without a read first.
When a request carries both headers, If-Match is evaluated first and If-None-Match second. On a key that does not exist, the failing If-Match decides and the answer is 412.
A domain can require conditional writes
Section titled “A domain can require conditional writes”A strict domain refuses single-key writes that carry neither header. The setting is made when the domain is created, in the key-value or the JSON engine, and stays with the domain:
curl -s -X POST http://127.0.0.1:3000/store-api/domains \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"name": "orders", "require_conditional_writes": true}'curl -si -X PUT http://127.0.0.1:3000/store-api/kv/orders/keys/o-1001 \ -H "Authorization: Bearer $KEY" -d '{"status": "open"}'HTTP/1.1 428 Precondition Requiredcache-control: no-store
428 Precondition Required: domain 'orders' requires If-Match or If-None-Match: * on writesThe same request with If-None-Match: * is accepted. The rule covers the single-key REST writes: PUT, DELETE and PATCH …/null on a key, PUT and DELETE on a JSON document. It does not cover bulk operations, such as the NDJSON import and the delete by prefix, or other write paths; the contract says so for both engines. Relational domains have no such setting.
| Status | Meaning |
|---|---|
400 |
If-Match is not a tag from a prior read, or If-None-Match is not *. Nothing is written. |
412 |
The precondition does not hold. Nothing is written; the answer carries the current ETag unless the key does not exist. |
428 |
The domain requires conditional writes and the request carries neither header. |
Bulk operations are atomic in steps, not as a whole
Section titled “Bulk operations are atomic in steps, not as a whole”-
A bulk import of JSON documents is applied in batches of 100 documents. Each batch is atomic: its documents appear to readers together, or not at all. The import as a whole is not. A document that fails its checks, or a line that is not valid JSON, is reported and skipped, and the rest goes on; an error of the storage aborts the call, and the batches already applied stay. The answer counts what happened:
Terminal window printf '%s\n' '{"_key": "o-2001", "status": "open"}' '{"_key": "o-2002", "status": "open"}' 'not json' \| curl -s -X POST http://127.0.0.1:3000/store-api/json/orders/bulk \-H "Authorization: Bearer $KEY" -H "Content-Type: application/x-ndjson" --data-binary @-{ "imported": 2, "failed": 1, "errors": [{ "key": "line 3", "error": "invalid JSON: expected ident at line 1 column 2" }] } -
A delete by prefix removes all matching keys in one atomic step, or none. A selection of more than
domains.max_bulk_delete_keyskeys is refused with413and nothing is deleted; with the limit at 5 and eight matching keys the answer is413 Payload Too Large: 8 keys match the selection, limit is 5.Terminal window curl -s -X DELETE "http://127.0.0.1:3000/store-api/kv/default/keys?prefix=tmp-" \-H "Authorization: Bearer $KEY"{ "deleted": 3 } -
A CSV import into a table imports row by row. A row that fails is logged and skipped, and the import goes on.
A bulk write takes no preconditions. The JSON import is limited in size by json.bulk_body_limit_bytes, the table import by rel.import_body_limit_bytes.
What a reader sees
Section titled “What a reader sees”- A confirmed write is visible to every read that starts after the answer. The answer is sent after the write reached the MemTable, see Write path.
- A search or a query sees one moment. A JSON search returns its documents and its
totalfrom one state, and aSELECTreads in one snapshot, also while writes and compactions go on. - Requests on one connection are processed in order, HTTP/2 included. A client that writes and then reads on one connection sees its own write; parallelism needs several connections.
- An export is not a snapshot. The export of a JSON domain streams documents: it lists the keys first and then reads one document after the other, so a document changed or deleted during the export is exported in its newer state or skipped.