Skip to content

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.