Analytics
A document read fetches records by key or filter. Analytical reads are a different shape: group-bys, aggregates, scans over millions of rows, usually against a warehouse or a pile of Parquet rather than your operational store. The analytics contract gives those a typed, governed home — without putting warehouse SQL in your handlers.
A named query, not a live DSL¶
Where the document port composes a query DSL at call time, an analytics
surface registers its queries up front. A handler names a query_key and passes typed
params; it never writes SQL or learns the backend:
class RegionTotal(BaseModel):
region: str
total: int
class SalesQuery(BaseModel):
min_total: int = 0
# The spec is the whole handler-facing surface: a named query + its params + read model.
# It says nothing about DuckDB, Parquet, or where the data lives.
SALES_SPEC = AnalyticsSpec[RegionTotal, None]( # type: ignore[reportUnknownReturnType]
name="sales",
read=RegionTotal,
queries={"by_region": AnalyticsQueryDefinition(params=SalesQuery)},
)
Running one returns typed rows, with the same shape × pagination naming as the document
port — run / run_page / run_cursor, plus project_run / select_run:
async def top_regions(ctx: ExecutionContext, min_total: int) -> list[RegionTotal]:
# The handler names the query and gets typed rows back — engine-agnostic.
page = await ctx.analytics.query(SALES_SPEC).run(
"by_region", SalesQuery(min_total=min_total)
)
return list(page.hits)
The physical mapping — the SQL, the warehouse table, the lake source — lives in the wiring, below the line. Swap DuckDB-over-Parquet for ClickHouse or BigQuery and the handler doesn't change.
Ingest¶
A surface can also be append-only writable: declare an ingest model and
ctx.analytics.ingest(spec).append(rows) bulk-loads them. There is no update or delete —
analytical data is immutable facts. To recompute a rollup over an ingested batch, run one
procedure rather than per-row writes.
When to reach for it¶
| You need | Use |
|---|---|
| Group-bys, aggregates, or scans over many rows | analytics |
| Fetch or list operational records by key / filter | document |
| Feed a value to logic inside a read source | query parameters |
| Recompute or run a command over the warehouse | procedures |
Encrypted columns can't be analyzed
A field-encrypted column is confidential but not aggregatable, groupable, or range-filterable — randomized ciphertext has no numeric or linguistic structure. Encrypt only the PII you store-and-return, never the dimensions and measures you query by.
The full spec and method surface is the analytics reference; the worked data-lake flow is the analytics-over-a-data-lake recipe.