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-up

From 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 survivors

Decoding 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.