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.
| RPC | Shape |
|---|---|
Query | SELECT, results inline (or spooled above a size threshold) |
QueryStream | The same request, streamed as row batches |
GetRow | Primary-key point read, optionally AS OF a Delta version |
BatchGetRows | Up 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 nolookup_tablesmap. Write literal table names. An overlay ∪ Delta view is registered for every table the statement references — joins, CTEs, derived tables,WHEREsubqueries — so cross-table SQL just works (dodil-tables/crates/reader/src/service.rs:1085-1098).
Query
Request
SQL
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.
manifestset meansrowsis 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 (formatis"arrow"or"parquet", with attl_secondslifetime). 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 shortlyThis 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 frame — TypedRows.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
- Execute → SELECT — SELECT through the SQL surface
- Upsert · Delete — the typed write RPCs
- Writing rows — the
INSERT/UPDATE/DELETE/MERGEstatements - SQL Compatibility — DuckDB dialect, statement shapes
- CLI Guide —
dodil data sql/dodil data table query