Skip to Content
We are live but in Staging 🎉

Query

Four read RPCs on dodil.tables.v1.Tables. All four take a db_id, all four return typed rows. See the Data hub for the full list.

RPCShape
QuerySELECT, results inline (or spooled above a size threshold)
QueryStreamThe same request, streamed as row batches
GetRowPrimary-key point read, optionally AS OF a Delta version
BatchGetRowsUp to 100 point reads in one call

Execute’s SELECT arm delegates to the same engine, so prefer Query / QueryStream on read-optimized paths.

There is no {table} placeholder and no lookup_tables map. Write literal table names. An overlay ∪ Delta view is registered for every table the statement references — joins, CTEs, derived tables, WHERE subqueries — so cross-table SQL just works (dodil-tables/crates/reader/src/service.rs:1085-1098).

Query

Request

SELECT event_type, COUNT(*) AS n FROM events GROUP BY event_type ORDER BY n DESC; -- joins need nothing special SELECT e.user_id, u.email, COUNT(*) AS clicks FROM events e JOIN users u ON e.user_id = u.id WHERE e.event_type = 'click' GROUP BY e.user_id, u.email;

Response

message QueryResponse { // Inline results, self-describing in both encodings. EMPTY when the // result spooled — read `manifest` instead. RowSet rows = 1; ServedBy served_by = 2; // New session watermark observed by this read. string max_wal_ulid = 3; repeated string warnings = 4; // Set when the result exceeded the inline threshold. ResultManifest manifest = 5; }

ServedBy names the tier that answered — SERVED_BY_HOT (RAM slot), WARM (disk slot), MERGED (overlay ∪ Delta — a strong read), WAREHOUSE (Delta scan — the analytical path), WAL, HOT_SHARDED. It is latency observability, not a correctness signal.

manifest set means rows is empty — never treat it as a zero-row result. Above a size threshold the plane spools result parts to object storage and returns presigned GETs (format is "arrow" or "parquet", with a ttl_seconds lifetime). Either surface the manifest to your caller so they fetch the parts directly, or fetch and re-inline them yourself.

Strong reads and WAL truncation

A strong read folds the un-drained WAL overlay over the Delta scan. That overlay has a per-call entry cap, and when it is hit the result would be incomplete — so the plane aborts rather than serving partial truth:

FAILED_PRECONDITION: strong read aborted: the WAL overlay truncated at <n> entries, so the result would be incomplete — compaction is catching up; retry shortly

This is a retry, not a bug. The writers’ drain hints fire at a fraction of the overlay cap, so by the time truncation is possible a drain is already scheduled and retries converge. Forcing it sooner: run Compact on the table. (dodil-tables/crates/reader/src/service.rs:1340-1355.)

There is no oltp_overlay_truncated boolean and no oltp_overlay_count on the public response — older docs describing a silent partial result are describing a shape that no longer exists.

QueryStream

Identical QueryRequest; the response is a stream of frames.

message QueryStreamResponse { RowSet chunk = 1; bool last = 2; // final frame, carries tail audit fields ServedBy served_by = 3; repeated string warnings = 4; }

Column metadata travels only in the first frameTypedRows.columns (or the Arrow schema message) is empty on continuation frames. Use this when a result would blow the inline response cap.

GetRow

Primary-key point read. This is the only read-side time travel on the surface.

rpc GetRow(GetRowRequest) returns (GetRowResponse); message GetRowRequest { string db_id = 1; string table = 2; RowSet key = 3; // the PK columns, typed, one row repeated string columns = 4; // projection (empty = all) // Delta time travel: read AS OF this version (absent = latest). A // pinned version is a PURE historical snapshot — no live overlay. optional int64 as_of_version = 5; optional string min_ulid = 6; ResultEncoding result_encoding = 7; } message GetRowResponse { bool found = 1; RowSet row = 2; // absent when not found ServedBy served_by = 3; string max_wal_ulid = 4; int64 version = 5; // the Delta version this read observed }

A point read always takes the STRONG path — read-your-writes is the point of it.

BatchGetRows

rpc BatchGetRows(BatchGetRowsRequest) returns (BatchGetRowsResponse); message BatchGetRowsRequest { string db_id = 1; string table = 2; RowSet keys = 3; // one row per key repeated string columns = 4; optional int64 as_of_version = 5; ResultEncoding result_encoding = 6; } message BatchGetRowsResponse { RowSet rows = 1; int64 version = 2; // Keys the plane could not serve this call (a shard moved mid-fetch): // retry exactly these. RowSet unprocessed_keys = 3; }

Caps are contractual: at most 100 keys and 16 MiB per request. Exceed either and the call fails INVALID_ARGUMENT — split the batch.

Absence is not an error. A key that is missing from rows and not in unprocessed_keys simply does not exist. A key in unprocessed_keys was not attempted — retry exactly those.

See also