Berserk Docs

Query gRPC API

Streaming query execution over gRPC

The query service exposes a gRPC API on port 9510 alongside the HTTP API. The gRPC API provides streaming query results, making it suitable for large result sets and real-time progress updates.

QueryService

service QueryService {
  rpc ExecuteQuery(ExecuteQueryRequest) returns (stream ExecuteQueryResultFrame);
}

ExecuteQuery returns a stream of frames. Clients receive schema, row batches, progress updates, and metadata as separate frames, allowing incremental rendering of results.

Frame Types

FrameDescription
schemaTable schema (sent before any row batches for that table)
batchRow data. Each batch has a result_iteration_id — when the ID changes, discard previous rows
progressExecution statistics: rows processed, chunks scanned/skipped, timing
doneQuery complete, close the stream
errorError with code, message, and source location
metadataWarnings and visualization hints

Result Iterations

Row batches carry a result_iteration_id. When a new iteration starts, all previous rows should be discarded. This enables incremental refinement of results. The is_iteration_complete flag on each batch indicates whether the iteration is fully delivered.

Proto Definition

// The query service executes BQL queries and streams results back to the client.
service QueryService {
  // Execute a BQL query and receive results as a stream of typed frames.
  // The stream delivers Schema, RowBatch, Progress, and Metadata frames,
  // terminated by a Completion frame or an Error frame.
  rpc ExecuteQuery(ExecuteQueryRequest) returns (stream ExecuteQueryResultFrame);

  // List the queries this pod is executing right now.
  //
  // Deliberately per-pod: a query runs on exactly one query pod, so the
  // cluster-wide view is the union across pods. The gateway fans this out over
  // the endpoint set it already resolves for balancing and merges the results,
  // which is what `bzrk ps` consumes. Answering from a single pod would show
  // only the fraction that landed there.
  rpc ListActiveQueries(ListActiveQueriesRequest) returns (ListActiveQueriesResponse);

  // Stop a query this pod is running, by the id `ListActiveQueries` reports.
  //
  // Cancellation is cooperative: the query stops starting new work at its next
  // boundary rather than being interrupted, so this returns as soon as the
  // signal is delivered, not when the work has wound down. The client running
  // the query sees its stream end with gRPC `CANCELLED` — distinct from a
  // Completion frame, so it can report the query as killed rather than done.
  //
  // Per-pod like the listing: the gateway fans out and exactly one pod matches.
  rpc CancelQuery(CancelQueryRequest) returns (CancelQueryResponse);
}

message CancelQueryRequest {
  string query_id = 1;
}

message CancelQueryResponse {
  CancelOutcome outcome = 1;
}

// Why a `CancelQuery` call ended the way it did.
//
// An enum rather than a bool: "not on this pod" and "here, but already
// finishing" are different answers an operator wants distinguished, and once
// this ships a bool could only be widened by adding a second field and
// deprecating the first.
enum CancelOutcome {
  CANCEL_OUTCOME_UNSPECIFIED = 0;

  // This pod is not running that query. The normal answer from every pod but
  // one when the gateway fans out, and not an error.
  CANCEL_OUTCOME_NOT_FOUND = 1;

  // The signal was delivered. Cancellation is cooperative, so the query stops
  // starting new work at its next boundary rather than immediately.
  CANCEL_OUTCOME_SIGNALLED = 2;
}

message ListActiveQueriesRequest {}

message ListActiveQueriesResponse {
  repeated ActiveQuery queries = 1;
}

// One executing query.
message ActiveQuery {
  // Server-side query id, and the handle `CancelQuery` takes.
  string query_id = 1;

  // Database the query resolves names against, as the client named it — the
  // same reference type `ExecuteQueryRequest` takes, rather than a rendered
  // string, so no server-side representation leaks into this contract.
  berserk.DatabaseRef database = 2;

  // How long it has been running.
  uint64 elapsed_ms = 3;

  // Since the last frame reached the client, and how many have. Elapsed time
  // says how long a query has run; this says whether it is still delivering.
  // A query four minutes in whose last frame was 200ms ago is working; one
  // whose last frame was three minutes ago is stuck, and `elapsed_ms` alone
  // cannot tell them apart. Equals `elapsed_ms` when nothing has been
  // delivered yet.
  uint64 idle_ms = 4;
  uint64 frames_sent = 5;

  // Which pod is running it. Filled in by the gateway as it merges, so a
  // merged listing stays attributable.
  string pod = 6;

  // Execution statistics so far — the same `Progress` message this query's own
  // client receives mid-stream, which is already the published contract for
  // these numbers. A parallel listing-only copy would only drift from it.
  // Unset before the first progress report: planning has not finished yet.
  optional Progress progress = 7;

  // The query as submitted, verbatim — the one field that answers "why is this
  // running" without a second lookup. Truncated to a bounded length by the pod
  // (with a trailing ellipsis) so one pathological client cannot make the
  // listing unreadable.
  string query = 8;

  // Who asked for it and from where. See `QueryOrigin`.
  QueryOrigin origin = 9;

  // What the query will scan. See `QueryWindow`.
  QueryWindow window = 10;
}

// The window a query scans, as the server resolved it.
//
// Not a claim and not a guess: these are the bounds the request carried, parsed
// and resolved against the server's captured `now`, and they are what the
// planner works from. A ten-minute lookback and a thirty-day one produce the
// same row otherwise — same elapsed time, same progress, wildly different
// amounts of work — so the span is what tells an operator whether a heavy query
// is heavy for a reason.
message QueryWindow {
  // Event-time (`timestamp`) bounds as nanoseconds since the Unix epoch. Both
  // set or neither: unset means the request imposed no range and the query
  // carries its own bounds in KQL, which the coordinator derives while
  // planning rather than here.
  optional sint64 time_range_start_nanos = 1;
  optional sint64 time_range_end_nanos = 2;

  // Ingest-time (`ingest_time`) bounds — the slicing axis — same shape. Set
  // independently of the event window: a query can bound either, both or
  // neither.
  optional sint64 ingest_time_range_start_nanos = 3;
  optional sint64 ingest_time_range_end_nanos = 4;

  // The `now` captured once when the request arrived, which every relative
  // bound (`24h ago`) resolved against. It is what makes an absolute window
  // readable as "the last hour" rather than a pair of timestamps.
  sint64 now_unix_nanos = 5;

  // This execution skips the map-reduce state cache on both read and write, so
  // it rescans a window the cluster has already summarized. Rare and
  // deliberate (benchmarking), and worth seeing next to an expensive row.
  bool bypass_map_reduce_state_cache = 6;
}

// Where a query came from, split by who asserted it.
//
// The split is the point of the message: an operator reading `bzrk ps` must be
// able to tell a fact the server established from a string the client typed
// about itself. Merging them into one flat block would make the trustworthy
// half indistinguishable from the decorative one.
message QueryOrigin {
  // Established server-side. Cannot be set by the caller.
  AttestedOrigin attested = 1;

  // Asserted by the caller about itself. Unverified.
  ClaimedOrigin claimed = 2;
}

// The half the server established.
//
// Gateway resolves the caller's credential and injects these as `x-bzrk-*`
// headers; the public edge strips that whole namespace on inbound, so a client
// cannot forge them. Fields are empty for callers that reached the pod without
// passing gateway (in-cluster service calls), never wrong.
message AttestedOrigin {
  // `"user"` | `"service"`.
  string principal_type = 1;

  // User id or service-principal id. The identity key; the name below is a
  // label that can change.
  string principal_id = 2;

  // Email for users, service-principal name for services.
  string principal_name = 3;

  // `"session"` | `"cli_token"` | `"service_token"` | `"trusted_proxy"` — how
  // the caller proved the principal, which is what separates a human at a
  // browser from a token running unattended.
  string authn_method = 4;

  // Row id of the proving credential. A correlation handle for audit, not an
  // authorization key.
  string credential_id = 5;

  // Originating client address as gateway resolved it (left-most
  // `X-Forwarded-For`, else the TCP peer). Empty when the query did not come
  // through gateway.
  string client_ip = 6;
}

// The half the caller asserted about itself.
//
// Free-form and unverified by construction — a client can put anything here,
// including nothing. Display and attribution only: nothing in the engine may
// branch on these values. Fields are length-capped and stripped of control
// characters by the receiving pod so a hostile client cannot corrupt an
// operator's terminal or bloat the registry.
message ClaimedOrigin {
  // Program that issued the query: `"bzrk-cli"`, `"grafana-plugin"`, `"ui"`.
  string app = 1;

  // Version of that program, so a bad query can be pinned to a release.
  string app_version = 2;

  // What the caller was doing: `"cli"`, `"dashboard"`, `"explore"`,
  // `"alerting"`, `"variable"`.
  string surface = 3;

  // The human behind a shared credential. Grafana authenticates every
  // dashboard in an org with one service token, so without this every panel in
  // the company is the same principal; the plugin fills it from the Grafana
  // login. Distinct from `AttestedOrigin.principal_name`, which is the identity
  // the server actually verified.
  string end_user = 4;

  // Grafana dashboard the panel lives on. The uid is what a URL is built from
  // (`/d/<uid>`); the title is what an operator recognises.
  string dashboard_uid = 5;
  string dashboard_title = 6;

  // Panel within that dashboard, same pairing as the dashboard fields.
  string panel_id = 7;
  string panel_title = 8;

  // The caller's own id for the request that issued this query. A dashboard
  // refresh fires one per panel under a single id, which is what makes a
  // refresh storm legible as one event rather than nine unrelated queries.
  string client_request_id = 9;

  // The agent driving the client, when one is: `"Claude Code"`, `"Codex"`.
  // A query nobody is watching is a different thing from one a person is
  // waiting on, and only the client knows which it is.
  string agent = 10;
}

// Request to execute a BQL query.
// Versions are listed in client preference order; zero is invalid.
message CapabilityOffer {
  string name = 1;
  repeated uint32 versions = 2;
  bool required = 3;
}

// Present, even with no offers, requests an explicit first-frame acknowledgement.
// Names: [a-z][a-z0-9_.-]{0,63}; at most 32 unique names and 16 unique versions/name.
message CapabilityNegotiationRequest {
  repeated CapabilityOffer offers = 1;
}

message CapabilitySelection {
  string name = 1;
  uint32 version = 2;
}

// Exactly one offered version per accepted capability; omitted optional offers are declined.
// Selections apply to this query only and cannot change after this frame.
message CapabilityNegotiationResponse {
  repeated CapabilitySelection selected = 1;
}

message ExecuteQueryRequest {
  // The BQL query string to execute.
  string query = 1;

  // Start of the time range (inclusive). Accepts relative expressions like "1h ago"
  // or absolute timestamps like "2024-01-01T00:00:00Z".
  string since = 2;

  // End of the time range (exclusive). Same format as `since`. Defaults to now.
  string until = 3;

  // IANA timezone name for time-based operations (e.g., "America/New_York").
  // Defaults to UTC if not specified.
  string timezone = 4;

  // Database to resolve unqualified table names against. Required.
  // Callers may send either a UUID (`identifier.id`) or a name
  // (`identifier.name`); the server resolves names once per request via
  // the metadata service. Empty / unset oneof rejected with
  // `InvalidArgument`.
  berserk.DatabaseRef database = 5;

  // INTERNAL — QCS→QC relay only. The fields below are set by the
  // query supervisor when it relays a query to a spawned per-query
  // coordinator child over its private UDS. They carry the captured
  // time semantics and the QWS member snapshot so the child neither
  // re-parses `since`/`until` (drift) nor re-reads the cluster
  // registry. The child does its own meta work (catalog, segment
  // listing) over its own connection. The public endpoint rejects
  // requests that set any of them with `InvalidArgument`.

  // The supervisor's captured now(), as nanoseconds since the Unix
  // epoch. When set, the child uses it verbatim instead of reading its
  // own clock, and `since`/`until` are ignored in favor of the
  // time-range nanos pair below.
  optional sint64 now_unix_nanos = 6;

  // Resolved query time range (inclusive start, exclusive end), as
  // nanoseconds since the Unix epoch. Both set or both unset. Optional:
  // when the API request carried no explicit range the child derives
  // the scan window from the query itself, exactly like in-process.
  optional sint64 time_range_start_nanos = 7;
  optional sint64 time_range_end_nanos = 8;

  // Postcard-serialized QWS cluster-member snapshot (the supervisor's
  // polled `ClusterPool` view) the child builds its frozen pool from.
  // The only meta-derived input relayed; everything else (catalog,
  // segment listing) the child fetches itself.
  optional bytes qws_members = 9;

  // Ingest-time window (the slicing axis), independent of `since`/`until`
  // (which bound event time). Same format as `since`/`until`. When set it
  // restricts `ingest_time` and combines with any in-query
  // `where ingest_time > ago(..)` bound (tighter wins). Optional on both
  // ends; an unset end means "up to now".
  string ingest_since = 10;
  string ingest_until = 11;

  // INTERNAL — QCS→QC relay only, the ingest-window counterpart of
  // `time_range_start_nanos`/`end_nanos`: the supervisor's captured, resolved
  // ingest window so the child doesn't re-parse `ingest_since`/`ingest_until`.
  // Both set or both unset.
  optional sint64 ingest_time_range_start_nanos = 12;
  optional sint64 ingest_time_range_end_nanos = 13;

  // Per-execution map-reduce-state-cache bypass. When true the coordinator
  // treats this one query as if no state cache were wired: it skips both the
  // reuse-read and the capture write-back, independent of the per-process
  // `map_reduce_state_cache_disabled` config. Forces a full cold scan of an
  // already-cached window for repeatable benchmarking, without evicting
  // anything (no race with concurrent queries). Default false.
  bool bypass_map_reduce_state_cache = 14;

  // INTERNAL — QCS→QC relay only. The run id the supervisor already published
  // to `bzrk ps`, so the child's coordinator adopts it instead of minting a
  // second one. Every task id is built from the run id, so without this the
  // child's whole log trail sits under an id no operator has. Ignored (and
  // rejected like the other internal fields) on the public endpoint.
  optional string internal_run_id = 15;

  // What the client says about itself — program, version, and the dashboard or
  // panel behind the query. Surfaced by `ListActiveQueries` and on the
  // query's completion record, so an operator seeing a heavy query can tell
  // where it came from. Optional, unverified, and never load-bearing: the
  // server never branches on it. The verified half of the origin is derived
  // from the caller's credential and cannot be sent here.
  ClaimedOrigin claimed_origin = 16;

  // Absent: legacy stream, no acknowledgement. Unsupported required offers fail
  // before execution. result_encoding version 1 is the existing snapshot stream.
  CapabilityNegotiationRequest capabilities = 17;
}

// A single frame in the streaming response for a query.
// A requested capabilities acknowledgement comes first. Then Schema frames,
// interleaved RowBatch and Progress frames,
// and finally a Completion or Error frame.
message ExecuteQueryResultFrame {
  // Unique identifier for this request, echoed from the server.
  string request_id = 1;

  oneof payload {
    // First frame when capabilities were requested, before any other frame.
    CapabilityNegotiationResponse capabilities = 9;

    // Table schema — sent once per table before any RowBatch frames for that table.
    TableSchema schema = 2;

    // A batch of result rows. Multiple batches may arrive for the same table and iteration.
    // When `result_iteration_id` changes, discard all previous rows and start fresh.
    RowBatch batch = 3;

    // Signals that the query has completed and no more frames will be sent.
    Completion done = 5;

    // Cumulative execution statistics. Sent periodically during query execution.
    // Each Progress frame supersedes the previous one.
    Progress progress = 4;

    // A query execution error. The stream ends after this frame.
    Error error = 6;

    // Warnings, partial failures, and visualization hints for the current result set.
    ResultMetadata metadata = 7;
  }

  // INTERNAL — QCS→QC relay only. Postcard-encoded authoritative engine
  // metadata (typed ExecutionStats, structural-type annotations,
  // visualization, exact error) the spawned coordinator child attaches
  // so the supervisor reconstructs results byte-exact with the
  // in-process path. Never set on the public endpoint; clients ignore it.
  optional bytes internal_sidecar = 8;
}

// Schema definition for a result table.
message TableSchema {
  // Table name (e.g., "PrimaryResult", "ExtraTable_0", or a fork branch name).
  string name = 1;

  // Ordered list of columns in this table.
  repeated Column columns = 2;
}

// A column definition within a table schema.
message Column {
  // Column name.
  string name = 1;

  // Data type of the column.
  ColumnType type = 2;

  // Whether the column may contain null values.
  bool nullable = 3;

  // Berserk extension. Optional structural type for `dynamic` columns
  // when the engine knows the inner shape (e.g. an `Array<Real>`
  // produced by `make-series`, or an `Object{...}` produced by
  // `bag_pack`). Absent means "opaque dynamic, infer by convention"
  // — the same shape ADX returns. Clients that don't know this
  // field ignore it (proto3 unknown-field tolerance) and continue
  // operating against a plain `dynamic` column type.
  optional StructuralType structural_type = 4;
}

// Structural type information for a `dynamic` column. Recursive
// because dynamics can be arrays of objects, objects with array
// fields, etc. Mirrors the engine-internal `AnnotationType` ADT.
message StructuralType {
  oneof kind {
    // Element is a scalar of the given type. The outer `Column.type`
    // is `dynamic`; this `scalar` carries the inner type.
    ColumnType scalar = 1;

    // Element is a homogeneous array of the given inner shape.
    // E.g. `make-series s = sum(v) on t step 5s` produces:
    //   - `s`: array_elem = scalar(LONG)   — array of longs
    //   - `t`: array_elem = scalar(DATETIME) — array of datetimes
    StructuralType array_elem = 2;

    // Element is an object (property bag) with named fields. Each
    // field carries its own structural type. E.g. `series_decompose`
    // returns `{baseline, seasonal, trend, residual}` each of which
    // is an array of reals.
    ObjectSchema object = 3;
  }
}

// Object schema for a `dynamic` column whose inner shape is a
// property bag with known named fields.
message ObjectSchema {
  repeated ObjectField fields = 1;
}

// One named field within an `ObjectSchema`.
message ObjectField {
  string name = 1;
  StructuralType type = 2;
}

// BQL data types for column definitions.
enum ColumnType {
  COLUMN_TYPE_UNSPECIFIED = 0;
  COLUMN_TYPE_BOOL = 1;
  COLUMN_TYPE_INT = 2;
  COLUMN_TYPE_LONG = 3;
  COLUMN_TYPE_REAL = 4;
  COLUMN_TYPE_STRING = 5;
  COLUMN_TYPE_DATETIME = 6;
  COLUMN_TYPE_TIMESPAN = 7;
  COLUMN_TYPE_GUID = 8;
  COLUMN_TYPE_DYNAMIC = 9;
}

// A batch of rows belonging to a single table and result iteration.
message RowBatch {
  // Name of the table this batch belongs to (matches a TableSchema.name).
  string table_name = 1;

  // Opaque identifier for the current result iteration. When this value changes,
  // all previously received rows for all tables must be discarded — the new iteration
  // represents a more complete result set that supersedes the previous one.
  string result_iteration_id = 2;

  // Rows in this batch. Column order matches the TableSchema for this table.
  repeated ValueRow rows = 3;

  // True when this is the last batch for this table in the current iteration.
  // Clients should show the first batch immediately for fast feedback, then
  // accumulate subsequent batches until this flag is true.
  bool is_iteration_complete = 4;

  // result_encoding v2 only: rows must be empty; decode this against the
  // previous completed result for this table before delivering this batch.
  RowDelta delta = 5;
}

// A chunk of a table replacement. Versions start at 1 per table per RPC.
// Every chunk of one iteration has the same version and base_version.
message RowDelta {
  uint64 version = 1;
  uint64 base_version = 2;
  uint32 row_offset = 3;
  uint32 row_count = 4;
  repeated RowDeltaOperation operations = 5;
}

message RowDeltaOperation {
  oneof operation {
    RowDeltaCopy copy = 1;
    RowDeltaInsert insert = 2;
  }
}

// Ranges refer to the immutable previous completed result, never this iteration.
message RowDeltaCopy {
  uint32 start = 1;
  uint32 count = 2;
}

message RowDeltaInsert {
  // Exact protobuf ValueRow bytes; equality includes all nested values.
  repeated bytes rows = 1;
}

// A single row of dynamically-typed values.
message ValueRow {
  // Cell values in column order. Each value corresponds to the column at the same
  // index in the TableSchema.
  repeated berserk.BqlValue values = 1;
}

// Cumulative execution statistics for the running query.
// Each Progress frame contains the total counts since the query started —
// always use the latest frame and discard earlier ones.
message Progress {
  // Total rows processed across all chunks.
  uint64 rows_processed = 1;

  // Total number of chunks in the query's time range.
  uint64 chunks_total = 2;

  // Chunks that were scanned (read and evaluated).
  uint64 chunks_scanned = 3;

  // Chunks skipped because their time range didn't overlap the query range.
  uint64 chunks_skipped_range = 4;

  // Chunks skipped by bloom filter (no matching values).
  uint64 chunks_skipped_bloom = 5;

  // Chunks skipped by shard hash (not matching the target shard).
  uint64 chunks_skipped_shard = 6;

  // Total predicate evaluations performed during scanning.
  uint64 predicate_checks = 7;

  reserved 8;
  reserved "bloom_checks";

  // True when the query completed early via short-circuit optimization.
  bool short_circuit_completion = 9;

  // Total uncompressed bytes of scanned chunk bodies.
  uint64 chunk_scanned_raw_body_size = 10;

  // Total uncompressed bytes of skipped chunk bodies.
  uint64 chunk_skipped_raw_body_size = 11;

  // Total compressed bytes of skipped chunks.
  uint64 chunk_skipped_compressed_size = 12;

  // Wall-clock time spent scanning chunks (nanoseconds).
  uint64 chunk_scan_time_nanos = 13;

  // Total query execution time (nanoseconds).
  uint64 query_time_nanos = 14;

  // Total compressed bytes of scanned chunks.
  uint64 chunk_scanned_compressed_size = 15;

  // Per-bin completion progress for `summarize ... by bin()` queries.
  optional BinProgress bin_progress = 16;

  // Time spent waiting in the query queue before execution started (nanoseconds).
  // Present when the query was queued due to concurrent query limits.
  optional uint64 queue_wait_nanos = 17;

  // Segment planning progress. Present during planning, absent after planning completes.
  optional PlanningProgress planning_progress = 18;

  // Total bytes of bloom filter data evaluated during bloom filtering.
  uint64 bloom_filter_bytes = 19;

  // Wall-clock time spent in merge and delivery across all query threads (nanoseconds).
  uint64 merge_time_nanos = 20;

  // Chunks that were scanned but yielded zero matching rows (false positives from pre-filtering).
  uint64 chunks_empty_scan = 21;

  // Chunks that encountered errors during row processing (e.g., type conversion failure).
  uint64 chunks_errored = 22;

  // Chunks skipped because required input fields were absent from the chunk schema.
  uint64 chunks_skipped_required_fields = 23;

  // Per-operator diagnostic telemetry in a stable key-value envelope.
  repeated OperatorDiagnostics operator_diagnostics = 24;

  // Chunks scanned where the range predicate could not be resolved at chunk level and
  // required per-row evaluation (i.e., the hoisted range did not fully cover the chunk).
  uint64 chunks_range_per_row = 25;

  // Number of merge+delivery invocations summed across all query threads.
  // Pair counter for `merge_time_nanos`.
  uint64 merge_count = 26;

  // Wall-clock time spent cloning reducer state during merge invocations
  // (nanoseconds, summed across all threads).
  uint64 reducer_clone_time_nanos = 27;

  // Number of reducer-state clones recorded into `reducer_clone_time_nanos`.
  uint64 reducer_clone_count = 28;

  // Cumulative wall-clock time the coordinator spent building intermediate
  // streaming snapshots (excludes the final result build). Nanoseconds.
  uint64 snapshot_build_time_nanos = 29;

  // Number of intermediate snapshot builds counted into
  // `snapshot_build_time_nanos`. Pair counter; consumers can compute the
  // average build duration that drives adaptive snapshot pacing.
  uint64 snapshot_build_count = 30;

  // Sum of thread CPU time spent inside the chunks arm of
  // `query_thread_loop` (nanoseconds, across all worker threads). Pair
  // with the chunks-arm wall (`worker_processing_time_nanos` engine-side):
  // wall ≫ CPU implies threads are parked on I/O / mutexes /
  // oversubscription; wall ≈ CPU implies CPU is the bottleneck.
  uint64 worker_chunks_arm_cpu_nanos = 31;

  // Sum of wall-clock time worker threads spent inside
  // `reader.fetch_chunk_data().await` — i.e. the cache_server fetch path
  // for chunk bytes. Nanoseconds, across all worker threads.
  uint64 worker_fetching_chunk_nanos = 32;

  // Sum of wall-clock time inside the synchronous chunk-body closure
  // (decompression + per-row scan + branch accounting). Nanoseconds,
  // across all worker threads. Pair with `worker_fetching_chunk_nanos`
  // to split chunk-arm wall into I/O wait vs scan body.
  uint64 worker_chunk_body_nanos = 33;

  // Generic counter bag (`CustomStats` from #2830). Lets engine emit
  // new measurements without a proto schema change for each one.
  // Keys are dotted lowercase namespaces — `bloom.short_circuit_chunks`,
  // `storage.bytes_fetched_cold`, `predicate.<i>.rows_kept`. Values are
  // sums across all worker threads; the coordinator merges per-key
  // before serializing.
  map<string, uint64> custom_stats = 34;

  // Cumulative cache-layer S3 retries observed answering this query.
  // Surfaced by `SegmentCacheTrait::fetch_stats_snapshot`; the
  // cache_server reports per-`OpenedItem` retries from its bounded
  // `get_cache_handle_inner` loop and the client tallies them on the
  // session. Non-zero is normal under flaky-backend conditions; a
  // sustained delta says the backend is the bottleneck.
  uint64 s3_retries_total = 35;

  // Cumulative cache-layer S3 give-ups for this query — one per cache
  // open that hit the bounded retry deadline or returned a
  // non-retriable error. Each give-up typically corresponds to a
  // PartialFailure for the segment it was trying to open.
  uint64 s3_giveups_total = 36;

  // Map-reduce-state cache: how many slices were seeded from cache
  // this run. Non-zero means at least one interior slice was reused
  // from a prior captured entry. Zero for unsliced queries, cold
  // queries, and queries where the cache is disabled / not eligible.
  uint64 map_reduce_state_cache_slices_reused = 37;

  // Map-reduce-state cache: total slices in the coordinator's slicing
  // grid for this run. Pair with `_slices_reused` for the per-run
  // reuse fraction. Zero means the cache was never consulted (not
  // sliced, no cache wired, or not a cacheable single branch).
  uint64 map_reduce_state_cache_slices_total = 38;

  // Opinionated query performance quality score, recomputed on every
  // snapshot. Absent until the coordinator attaches it.
  optional QueryQuality quality = 39;
}

// Query performance quality score. Every field is [0;1] with 1.0 = good;
// clients render as a percentage or however they like. See
// docs/dev/query-quality-score.md.
message QueryQuality {
  // Headline = scanning * transformation * aggregation.
  float total = 1;
  // How well bloom/range/shard filters let us skip load+decompress.
  float scanning = 2;
  // Per-row mapper work; 1.0 = no transforms.
  float transformation = 3;
  // Reducer weight; 1.0 = light/bounded.
  float aggregation = 4;
  // Blend weight: 0 = pure plan estimate, 1 = pure live-stats measure.
  float measured = 5;
}

// Diagnostic telemetry for a specific query operator.
message OperatorDiagnostics {
  // Operator kind (e.g., "summarize", "join").
  string kind = 1;

  // Operator ID within the query plan.
  uint32 operator_id = 2;

  // Key-value diagnostic entries.
  repeated KeyValue values = 3;
}

// A string key-value pair.
message KeyValue {
  string key = 1;
  string value = 2;
}

// Segment planning progress.
message PlanningProgress {
  // Number of segments that have completed planning.
  uint64 segments_done = 1;

  // Total number of segments to plan.
  uint64 segments_total = 2;
}

// Per-bin completion progress for `summarize ... by bin()` queries.
message BinProgress {
  reserved 4;
  reserved "overall_percent";

  // Start value of the first bin boundary.
  // Bin N covers [first_bin_start + N * bin_span, first_bin_start + (N+1) * bin_span).
  sint64 first_bin_start = 1;

  // Width of each bin.
  uint64 bin_span = 2;

  // Completion percentage (0-100) for each bin, one byte per bin.
  // Index i corresponds to bin i. Value 100 means fully scanned.
  bytes completion_percentages = 3;

  // Numerator of overall completion: sum across slices of the number of
  // segments whose planning has finished. Paired with overall_total to
  // form the segment-weighted overall progress — larger slices (more
  // overlapping segments) contribute proportionally more than small
  // slices, which matches actual work better than a flat arithmetic mean
  // across the per-bin bars. Both zero when no segment totals are known
  // yet (initial planning still running).
  uint64 overall_done = 5;

  // Denominator of overall completion: sum across slices of the total
  // segments overlapping that slice. Zero when unknown — clients should
  // treat (0, 0) as "no overall percentage available yet".
  uint64 overall_total = 6;
}

// Signals that the query has completed successfully.
// How a query ended. Every value but a fault is a controlled completion: the
// answer is well-defined, bounded by wherever the query stopped. A client that
// wants only unbounded answers checks for COMPLETED; the rest still carry
// results and stats worth using, and it is the client's call whether a result
// bounded that way suits its task.
enum QueryOutcome {
  QUERY_OUTCOME_UNSPECIFIED = 0;
  // Ran to the natural end of its input.
  QUERY_OUTCOME_COMPLETED = 1;
  // The input asked for no more — a satisfied `take N`.
  QUERY_OUTCOME_COMPLETED_EARLY = 2;
  // The submitting client hung up or asked to stop. It already knows.
  QUERY_OUTCOME_STOPPED_BY_SUBMITTER = 3;
  // An operator stopped it. The client did not ask for this.
  QUERY_OUTCOME_STOPPED_BY_OPERATOR = 4;
  // The server stopped it deliberately: an admission limit, a quota, a
  // shutdown. Working as intended — nothing to report.
  QUERY_OUTCOME_STOPPED_BY_SYSTEM = 5;
  // The deadline ran out.
  QUERY_OUTCOME_DEADLINE_EXCEEDED = 6;
  // The server stopped it because something went wrong on our side. The
  // result, if any, is partial. This says only that we consider the stop a
  // failure — not what failed — so a client knows this one is worth
  // reporting, citing the query id, where the outcomes above are not.
  QUERY_OUTCOME_STOPPED_BY_FAULT = 7;
}

message Completion {
  // Why the stream ended. Absent on servers predating this field, which is
  // indistinguishable from UNSPECIFIED — treat that as COMPLETED.
  QueryOutcome outcome = 1;
}

// A query execution error.
message Error {
  // Error code discriminator (e.g., "UnknownFunction", "TypeMismatch").
  string code = 1;

  // Brief error title.
  string title = 2;

  // Support ticket ID for tracking.
  string support_ticket_id = 4;

  // Human-readable error message with source annotations.
  string message = 5;

  // Source code location where the error occurred.
  Location location = 7;

  // Structured error details as JSON.
  string details = 8;
}

// A range within the query source text.
message Location {
  uint32 start_byte = 1;
  uint32 end_byte = 2;
  uint32 start_line = 3;
  uint32 start_column = 4;
  uint32 end_line = 5;
  uint32 end_column = 6;
}

// A partial failure for one or more segments that could not be read.
message PartialFailure {
  // OBSOLETE — always empty, never populated. Segment ids are internal
  // storage identifiers no client can act on; only the node holding the
  // segments needs them, and it logs them locally against the query's
  // trace id. The field stays in the schema so existing generated clients
  // keep compiling; read `segment_count` instead.
  repeated string segment_ids = 1 [deprecated = true];

  // Human-readable error description.
  string message = 2;

  // How many segments this failure covers — enough to judge how much of
  // the result is missing.
  uint64 segment_count = 3;
}

// Visualization metadata from the `render` operator.
message VisualizationMetadata {
  // Visualization type (e.g., "table", "timechart", "piechart", "linechart").
  optional string visualization_type = 1;

  // Visualization properties (e.g., x-column, y-columns, legend).
  map<string, string> properties = 2;
}

// Advisory highlight pattern extracted from a positive text predicate.
// Best-effort only: the engine ships the patterns it filtered on, not the
// spans it actually matched. A missed or spurious highlight is not a
// correctness bug. Renderers that cannot honor a hint should skip it.
message HighlightHint {
  // Literal to mark (or the regex anchor / pattern when `regex` is set).
  string term = 1;

  // equality | substring | prefix | suffix | word | word_prefix | word_suffix
  string kind = 2;

  bool case_sensitive = 3;

  // Optional predicate field scope (unscoped search / wildcard is unset).
  optional string field = 4;

  // Optional regex pattern source. A renderer whose regex flavor rejects
  // the pattern falls back to marking `anchor` (or `term`).
  optional string regex = 5;

  // Optional literal extracted for the SIMD prescan / fallback mark.
  optional string anchor = 6;

  // Output column names this hint applies to.
  repeated string columns = 7;
}

// Metadata for the current result set.
message ResultMetadata {
  // Data is MISSING from the result: rows the query should have covered were
  // not read (unreachable segments, an unavailable stream group, a lost
  // worker, a deadline cut). Consumers must treat the result as incomplete —
  // the completeness watermark and result caching are suppressed whenever any
  // entry is present. Contrast with `warnings`.
  repeated PartialFailure partial_failures = 1;

  // Visualization hints from the `render` operator, if present.
  optional VisualizationMetadata visualization = 2;

  // Advisory only: the result is COMPLETE, but how the query ran deserves
  // attention (e.g. "summarize memory limit reached", "result truncated",
  // planner fallbacks). Anything that implies missing rows belongs in
  // `partial_failures`, never here.
  repeated QueryWarning warnings = 3;

  // Advisory query-term highlights. Sent once alongside the schema.
  repeated HighlightHint highlights = 4;
}

// A warning produced during query execution.
message QueryWarning {
  // Operator ID for source correlation.
  uint32 operator_id = 1;

  // Branch index for fork queries (0 = main branch).
  optional uint32 branch_index = 2;

  // Warning kind discriminator (e.g., "SummarizeMemoryLimit", "ResultTruncated").
  string kind = 3;

  // Source location where the warning originated.
  optional Location location = 4;

  // Human-readable warning message.
  string message = 5;

  // Structured warning details as JSON.
  string details = 6;
}

On this page