Aggregate roll-ups
Your compressed archive is now also an analytical store. A whole-column SUM / MIN / MAX / COUNT is answered from a compact per-row-group roll-up kept in the archive footer — it decodes zero data blocks. You read the footer, not the columns, and the same bytes still reconstruct the exact original.
Roll up a column
at1 agg trades.at1 price --op sum # -> reads a compact per-row-group roll-up in the FOOTER — decodes ZERO data blocks # in a small demo, sum(price) read ~60 footer bytes of a 511-byte file (no block decode) at1 agg trades.at1 price --op min at1 agg trades.at1 price --op max at1 agg trades.at1 0 --op count # column by name OR index
- SUM is exact integer / fixed-point arithmetic — decimals are summed in scaled-integer space, never floating-point rounding.
- MIN / MAX come straight from the existing zone-map.
- COUNT is the row count.
- By name or index: name the column (a qjson field or a qcolumnar header row) or give its index.
In a small demo, sum(price) read ~60 footer bytes of a 511-byte file with no block decode at all. On large archives the I/O saving scales with the data — you read the footer, not the columns.
Filtered roll-ups — --where
Add a predicate and the same zone-map that powers query pushdown skips non-matching row-groups, then aggregates only the survivors. Still pushed down — not a full scan.
# a predicate uses the existing zone-map to SKIP non-matching row-groups, # then aggregates only the survivors — still pushed down, not a full scan at1 agg trades.at1 price --op sum --where ts:1704067200000:1704067210000
Optional & backward-compatible
The roll-up is an additive footer stream. An older decoder simply ignores it and still reconstructs the file byte-for-byte. Archives written without the roll-up fall back to an exact full-scan aggregate, so the same answer comes back either way.
From the SDK
from at1_reader import AT1Reader
r = AT1Reader("trades.at1")
total = r.aggregate("price", "sum") # exact fixed-point sum
lo = r.aggregate("price", "min") # from the zone-map
n = r.aggregate("price", "count") # row count
hot = r.aggregate("price", "sum", where={"ts": (lo, hi)}) # filtered roll-upFrom an AI agent (MCP)
The MCP server exposes the same primitive as a typed tool, so an agent can roll up a column without rehydrating the archive:
at1_aggregate(at1_path="trades.at1", column="price", op="sum")
# -> exact total, read from the footer roll-up — no block decode
at1_aggregate(at1_path="trades.at1", column="price", op="sum",
where={"ts": [1704067200000, 1704067210000]})
# -> zone-map skips non-matching row-groups, aggregates the survivorsDecoding and verifying are always free and never need an account; the encode path is metered against the account whose API key the host process supplies — same as the rest of AT-1. Reading the roll-up is part of decode: free.