The host bindings follow the same conceptual surface while respecting host naming and object ownership.
| Capability | Rust | Python | Node.js | WASM |
|---|---|---|---|---|
| Parse script | Parser::parse_all | parse | parse | parse |
| Validate | Parser::parse(_).is_ok() | is_valid (parse + plan) | isValid (parse + plan) | isValid |
| Tokenize | lexer API | tokenize | tokenize | tokenize |
| Inject policy | inject_filter | inject_filter | injectFilter | inject_filter / Stmt.injectFilter |
| Explain | planner/runtime | explain | explain | explain / analyze |
| Execution profile (client timings plus server time and usage) | Executor::explain_analyze | Client.explain_analyze | Client.explainAnalyze | Client.explainAnalyze |
| DB-API cursor (Python) | — | pyqql.connect + cursor (execute/executemany/fetch*/nextset, description, rowcount) | — | — |
| Lazy scroll cursor | manual SCROLL ... AFTER :cursor loop (see Rust SDK page) | Client.scroll_cursor (sync generator) plus Client.scroll_cursor_async (async generator) | Client.scrollCursor / scrollCursor, Client.scrollStream / scrollStream | dx.js scrollCursor helper over Client.execute (see WASM SDK page) |
| Compile | planner/runtime | compile_query | compileQuery | compile |
| Bind parameters | bind_named / bind_positional (+ *_readable) | bind, Stmt.bind, execute(…, params=) | bind, Stmt.bind, execute(…, { params }) | bind, Stmt.bind, execute(…, { params }) — typed arrays convert directly |
| Execute | Executor::execute | Client.execute | Client.execute | Client.execute |
| Parse to JSON string | — | parse_json | parseJson | parseJson |
Bulk ingest (:rows template, chunked) | Executor::upsert_many | Client.upsert_many | Client.upsertMany | Client.upsertMany |
| Typed hits shortcut | — | Client.execute_hits / execute_async_hits | Client.executeHits / module executeHits | dx.js executeHits (wrap ExecutionReport for the rest) |
Three input axes
Section titled “Three input axes”- One statement: the strict single-statement parser.
- One script: semicolon-delimited source parsed as one ordered unit.
- Host batch: an array of independent statements or scripts.
These are not interchangeable APIs. In particular, an array boundary is owned by the host, while semicolon ordering is part of QQL source.
Statement families (language surface)
Section titled “Statement families (language surface)”Hosts expose the same statement set the planner accepts. Schema and cluster administration include:
| Family | Statements |
|---|---|
| Retrieval | QUERY …, SCROLL, COUNT |
| Data | UPSERT, UPDATE … SET VECTOR/PAYLOAD, DELETE, CLEAR PAYLOAD, DELETE PAYLOAD, DELETE VECTOR |
| Schema | CREATE/ALTER/DROP COLLECTION, CREATE/DROP INDEX, CREATE/DROP SHARD KEY |
| Inspect | SHOW COLLECTIONS, SHOW COLLECTION, SHOW SHARD KEYS, SHOW QUOTAS |
| Cluster (REST) | SET QUOTA (…) [WAIT bool] |
SHOW QUOTAS / SET QUOTA plan to REST /quotas only. gRPC and edge reject them with stable codes (QQL-GRPC-QUOTA, QQL-EDGE-UNSUPPORTED-QUOTA). Transport metadata such as route affinity is configured on the client — Rust RestQdrant::with_route_affinity / GrpcQdrant::with_route_affinity, pyqql.Client(route_affinity=…), nqql new Client({ routeAffinity }), and WASM client.setRouteAffinity(key) — not as QQL syntax. Edge has no route affinity (single node).
Statement ownership
Section titled “Statement ownership”Python, Node.js, and WASM expose statement handles so a host can parse once, inject policy, set optional shard routing, inspect the AST, and execute the statement. In addition to toObject/toJSON, Stmt across Python, Node, and WASM exposes bind(params) to substitute :name / ? placeholders into the statement AST and compile_route / compileRoute (optionally accepting params) to inspect the compiled REST route of a single statement. WASM owners must call free() explicitly when they are finished.
Token objects are consistent across hosts: Python, Node, and WASM bindings emit { kind, text, pos, end, len }, matching the underlying Span { start, end } model.
Execution reports
Section titled “Execution reports”Execution returns a stable report with aggregate ok, ordered per-operation results, and succeeded/failed counts. on_error / onError selects stop or continue behavior for multi-operation inputs. Python, Node, and WASM reports add typed accessors through ExecutionReport (pyqql, dx-common.js, dx.js): hits(stmt) / points(stmt) return ScoredPoint objects (id, score, payload, text, collection), facet(stmt) returns normalized [{ value, count }] hits, count(stmt) returns an integer, and groups(stmt) returns backend group objects (see below). Python execute_hits / execute_async_hits, Node executeHits, and WASM dx.js executeHits(client, query, options) skip the report entirely. Passing params as a list/array with one entry per statement binds parameters statement-scoped: the length must match the statement count exactly, and each entry is a dict/object (named) or a list/array of scalars (positional). The WASM Client is REST-only: it compiles to and executes Qdrant REST routes and does not open gRPC connections; wrap its plain { ok, results, succeeded, failed, telemetry } object with the dx.js ExecutionReport for typed hits() / facet() / count() / groups() accessors, telemetry reads, and the executeHits(client, query, options) one-shot.
Statement indexing
Section titled “Statement indexing”Every typed accessor takes an optional statement index with Python list semantics on all hosts: 0 is the first statement (the default), -1 is the last statement, and any out-of-range index returns an empty list (hits, points, ids, facet, groups) or 0 (count) instead of raising. This applies to pyqql.ExecutionReport, Node ExecutionReport, and WASM dx.js ExecutionReport alike. Rust callers use report.hits(0) style helpers with usize indexes on ExecutionReport.
Groups shape
Section titled “Groups shape”groups(stmt) returns the raw backend group list for a GROUP BY query, normalized across the {"result": {"groups": [...]}} and bare {"groups": [...]} envelopes. Each entry is { "id": <group key>, "hits": [<point records>] } where id is the group key value and hits holds the ordered point records for that group. Out-of-range indexes return []. See the Python, Node, and WASM SDK pages for per-host examples.
Telemetry
Section titled “Telemetry”Every ExecResponse may carry telemetry with server time and usage when the backend reported it: { "time_s": <seconds or null>, "usage": <hardware and inference usage or null> }. Missing telemetry reads as None (Python), null or undefined (Node, WASM), never as an error. The report also carries aggregated telemetry (server times summed, hardware counters and per-model tokens merged). explainAnalyze / explain_analyze returns the same telemetry plus server_time_s and usage top-level fields alongside plan, phases, and results. See the Python SDK, Node.js SDK, WebAssembly SDK, and Rust SDK pages.
Bulk ingest contract (upsertMany / upsert_many)
Section titled “Bulk ingest contract (upsertMany / upsert_many)”One :rows template is prepared once, then each batchSize / batch_size chunk splices through the point-splice path with no re-parse and no per-batch schema fetch. The contract is identical on Python, Node, and WASM (see the per-SDK pages for call shapes):
rowsmust be an array or list of point objects ({id, vector, ...payload}). Any other top-level shape fails closed withQQL-BIND-TYPE-MISMATCH.- Each point has the same shape as an inline
VALUES {...}row. A missingidfails withQQL-VALIDATION-UPSERT-ID; any other misshapen row fails withQQL-BIND-TYPE-MISMATCHinstead of landing partial data. - Row vectors accept the same shapes everywhere: plain lists or arrays of numbers, packed 1-D float buffers or typed arrays (
numpy,array.array,memoryviewon Python;Float32Array/Float64Arrayon Node and WASM, one copy with no per-element walk), integer typed arrays or lists for sparseindices(Int32Array/Uint32Arrayon Node and WASM), and the flat{data, dim}(Python{"data": [...], "dim": N}) multivector form. - Raw binary without a float view fails closed with a wrap-first typed error: wrap the bytes in a
Float32ArrayorFloat64Arrayview first (Pythonbytesandbytearraytake the same path: pass a float buffer instead). On Node and WASM the code isQQL-BIND-INVALID-PARAMS; on Python it is aValueErrorwith the same guidance. batchSize/batch_sizemust be an integer greater than or equal to1. Values below1fail closed withQQL-VALIDATION-UPSERT-BATCHbefore any I/O.- Empty
rowsreturns an emptyokreport without I/O.
Embedder capability ladder
Section titled “Embedder capability ladder”HttpEmbedder configuration is not portable across hosts. Use this table before assuming a dense config covers multi-vector, image, or rerank work:
| Capability | Node HttpEmbedder | Python HttpEmbedder | WASM client |
|---|---|---|---|
Dense text (endpoint, model, dimension) | yes | yes (dense settings; sparse resolves to local BM25) | yes via setHttpEmbedder(endpoint, model, dimension) |
Multi or ColBERT (multiEndpoint, multiModel, multiDimension) | yes | yes | yes via setHttpMultiEmbedder(endpoint, model, dimension) |
Image or CLIP (imageEndpoint, imageModel, imageDimension) | yes | yes | yes via setHttpImageEmbedder(endpoint, model, dimension) |
Cross-encoder rerank (rerankEndpoint, rerankModel) | yes | yes | yes via setHttpReranker(endpoint, model) |
JS callback (async (texts) => number[][], batched) | no (native transport) | no | yes via setEmbedder(fn) |
WASM accepts HTTP dense, multi-vector, image, and reranker endpoints plus one JS callback; use one setter per group. setRemoteEmbedder on WASM is an alias for setHttpEmbedder.
Error shape
Section titled “Error shape”Every host surfaces the same typed error fields: code (stable QQL-* string), kind (Lex, Parse, Validation, Execution, Transport, Backend), span (byte offsets or null), plus structured fields and request_id when the transport provides them. Python raises QqlError subclasses (QqlSyntaxError, QqlValidationError, QqlExecutionError, QqlTransportError, QqlBackendError); Node and WASM reject or throw Error objects with .code / .kind / .span / .fields / .request_id via buildError; Rust returns QqlError values.