Skip to content

Analytics

The analytics contract runs registered, parametrized queries against a warehouse table or view and returns typed rows, with an optional append-only ingest side. Unlike the document port it has no per-row CRUD and no live query DSL — you register named queries up front and pass typed params; unlike procedures it only reads. The concept is Analytics; the worked flow is the analytics-over-a-data-lake recipe.

rows = await ctx.analytics.query(spec).run("by_region", params)   # registered query → typed rows
await ctx.analytics.ingest(spec).append(new_rows)                 # append-only bulk load

Spec

AnalyticsSpec[R, Ing] — the read-row model, the named queries, and an optional ingest model:

Field Type Default Meaning
name str \| StrEnum required logical name / warehouse route
read type[R] required default read model for result rows
queries Mapping[str, AnalyticsQueryDefinition] required named queries (≥1); each declares a typed params model
ingest type[Ing] \| None None append-row model; None disables ingest
provenance AnalyticsProvenance UNDECLARED where these rows come from — see below
encryption FieldEncryption \| None None seal columns at rest (confidential, not aggregatable; binds_record_id unsupported — rows have no id)
read_codec / ingest_codec ModelCodec \| None None codec overrides (auto-derived otherwise)

Each AnalyticsQueryDefinition carries a Pydantic params model — the typed arguments a run* call passes — and an optional description.

Provenance

provenance says whether this warehouse table derives from data the application already owns, or is the only place those rows exist. The framework cannot work that out for itself — both cases have the same spec, the same ports and the same rows — and guessing is unsafe in either direction, so the author declares it:

Value Meaning
PROJECTED recomputed from a plane that is itself exported (documents, say). A portable export does not carry it; the application recomputes it on the target.
SYSTEM_OF_RECORD the warehouse holds the only copy — the usual shape when events are ingested straight into ClickHouse or BigQuery. A portable export refuses: the query port exposes only your named queries, so there is no full-scan read to carry it with and nothing to rebuild it from. Use your warehouse's own tooling.
UNDECLARED (default) nobody has said. Legal at runtime — it changes nothing about how the port behaves — but a portable export refuses rather than guess.

It costs nothing until you try to run a portable export of the application: assume projected wrongly and the export silently drops the only copy of the data; assume system of record wrongly and it refuses to carry a table that was never more than a cache. "We didn't think about it" must not look like "there was nothing here."

Query port (ctx.analytics.query(spec))

Run a registered query by key with typed params; the shape × pagination naming mirrors the document query port:

Method Result
run(query_key, params, pagination=None, *, options=None) CountlessPage[R]
run_page(...) Page[R] (with .count when the backend supports it)
run_cursor(...) CursorPage[R] (keyset)
run_chunked(..., fetch_batch_size=2000) async generator of row batches
project_run* / select_run* JsonDict / caller-typed-model variants

options: AnalyticsRunOptions carries the per-run knobs — dry_run, max_rows, and timeout.

Ingest port (ctx.analytics.ingest(spec))

Method Signature Notes
append append(rows) append-only bulk insert; returns an AnalyticsAppendResult (accepted / rejected / errors), or None when the backend reports nothing

Ingest is append-only — no update or delete. To recompute a rollup over an ingested batch, reach for the procedures port rather than per-row writes.

Implemented by

Backend Notes Integration
Postgres tables / views Postgres
ClickHouse columnar warehouse ClickHouse
BigQuery serverless warehouse BigQuery
DuckDB in-process / data-lake (query-only; tenancy ceiling tagged) DuckDB