Documentation menu
QQL reference
Use bound values, nested fields, joins and conditional writes through the common transaction engine.
On this page
Filter, sort and page
Submit QQL with op: "query" and a bindings object. Bind values instead of interpolating strings. LIMIT defaults to 100; LIMIT plus OFFSET must fit the current 10,000-row working window. Separate page requests do not pin a common snapshot.
SELECT title, status FROM projects
WHERE workspace = :workspace AND status = "active"
SORT title ASC
LIMIT :pageSize OFFSET :offset Exact literals and scientific notation
QQL literals and lossless JSON bindings share one exact parser. Scientific notation preserves admitted digits: 1e-400 remains nonzero and 1e400 is representable. Numerically equal integer and decimal forms share comparison and unique-index semantics.
Input text and expanded ordinary-decimal text each fit 4,096 bytes, including sign and decimal point. Oversized values are rejected before expansion. LIMIT and other integer-only clauses require integers. Browser clients reject values outside their exact JavaScript numeric boundary; use the lossless CLI or Rust interface for those bindings and results.
Older QQL exponent literals passed through floating point. New parsing does not repair previously rounded records or change committed receipts. Explicit floating-point values and vector arithmetic remain approximate; AVG currently discards the remainder after eighteen additional decimal places.
INSERT INTO measurements {n: 9007199254740993e0};
SELECT * FROM measurements WHERE n = 9007199254740993; Join related collections
Joined fields live under the joined collection name. ON accepts 1–16 equality pairs joined by AND, including nested object paths and fields from earlier joins. Repeated pairs are removed in first-occurrence order. Every pair must match; exact numbers and explicit null retain their value semantics, while missing fields and object/array-valued keys do not match.
The planner receives the complete tuple and can use a compound index even when ON lists its fields in another order. Probes are reused for each distinct tuple; without a usable index, one right-side scan builds a tuple lookup. All candidates pass the equality checks. ON does not add expanded-array correlation or OR joins. Only safe base predicates move before the join. Search syntax does not imply a persisted BM25 index.
SELECT title, categories.meta.region FROM items
JOIN categories ON tenant = categories.tenant AND category = categories.id
WHERE enabled = true AND categories.meta.region = :region
SEARCH categories.label MATCH :term
SORT categories.label ASC
LIMIT 20 Query arrays and nested fields
CONTAINS_ALL and CONTAINS_ANY require an array on both sides. Duplicate members do not imply multiplicity. An empty right array satisfies ALL for an actual array field and never satisfies ANY. Expanded index paths such as passkeys[*].credentialId have bounded traversal and key expansion.
SELECT _key FROM roles
WHERE capabilities CONTAINS_ALL :routes
LIMIT 100 Change records conditionally
Update expressions read the pre-update row and support literals, bindings, fields, addition and COALESCE. Numeric addition preserves the exact number model and rejects nonnumeric operands. Set expectedCount on the enclosing query operation for a transactional precondition. DELETE follows the same predicate and mutation pipeline.
UPDATE projects
SET title = :title, edits = COALESCE(edits, 0) + 1
WHERE _key = :key AND _revision = :revision Merge partial objects and edit arrays
Object assignments recursively merge omitted fields and nested siblings. Null stores null. Scalars and arrays replace the assigned value; REPLACE(expr) explicitly replaces an entire object. All expressions read the row before the UPDATE.
MERGE(left, right) combines objects. ARRAY_APPEND(array, additions) preserves order and duplicates. ARRAY_MERGE and ARRAY_REMOVE select every matching object element by an explicit field path and exact value; missing fields do not match. No implicit array identity is guessed.
UPDATE users
SET profile = {locale: "fr"},
contacts = ARRAY_MERGE(contacts, "id", :contactId, :patch)
WHERE _key = :key Run multiple statements and bulk inserts
Bind projects to an object or a nonempty array of up to 10,000 objects. Semicolons separate statements outside quoted strings. The execute request is one atomic transaction: later statements see earlier staged writes and any failure rejects the entire batch.
Multiple statements return op: "query_batch" with ordered results, while outer result count still matches the operations array. Native operations plus QQL static nodes together are limited to 64 per request. Existing byte and record limits also apply. Use separate query operations in the same request when each statement needs expectedCount.
The generated HTTP and WebSocket clients support programs through execute(). The defineQql helper remains for one statement and one row set. Scoped LET/FOR programs can feed retained query results into later statements. A fluent structured builder remains planned.
INSERT INTO projects :projects;
UPDATE projects SET profile = {locale: "fr"} WHERE workspace = :workspace;
SELECT * FROM projects WHERE workspace = :workspace SORT title LIMIT 20; Reuse query results with scoped loops
LET binds a typed value or SELECT result. FOR iterates an array or a parenthesized SELECT result in order. External inputs use :name; local values use $name or $name.path. Collection names, fields and operators stay static. Sources are evaluated once when reached and retain their values while later statements see staged writes. The body is parsed once and binds typed inputs into the same planner per iteration. SELECT sources keep typed projection and aggregate results without a JSON round trip.
Source SELECT results include their selected fields plus _key and _revision. Names are case-sensitive, declared before use and scoped to their block; redeclaring an active name is rejected. Missing object paths resolve to null. Nested FOR blocks iterate nested arrays. LET and source SELECTs emit no output; query_batch contains executed statement results in iteration order. Empty loops emit none.
The execute request permits 64 static nodes, 10,000 dynamic steps, one million query-work units and 4 MiB of results. Loop nesting is limited to 16 levels. Local values and binding expansion each have a conservative 16 MiB allocation budget. Every failure rolls back the request. All static resources are authorized even for empty loops. Known inputs without object links can be written with collection access alone; linked inputs, SELECT-derived locals and copied record fields retain the conservative object-read requirement. Scoped programs do not accept expectedCount. Performance acceptance remains open.
LET selected = (SELECT * FROM projects WHERE workspace = :workspace);
FOR project IN $selected {
FOR patch IN :patches {
UPDATE projects SET profile = $patch WHERE _key = $project._key;
};
};
SELECT title, profile FROM projects WHERE workspace = :workspace; Inspect access behavior
Primary keys are indexed. Declared secondary equality, safe prefixes and dense scalar ranges can narrow candidates. Every candidate still passes the residual predicate. OR requires a complete candidate source for both branches before using an index union.
Sorting and operator work remain bounded. A complete cost-based optimizer, universal index-order streaming, persisted BM25/HNSW and all-query performance superiority are not completed features.