Continuous aggregates: partial state, watermark refresh, invalidation (spec §8).
Aggregates store count/sum, never avg — the rule that makes
hierarchical rollups compose and re-aggregation over arbitrary windows
stay correct (§8.2). Refresh aggregates only (watermark, now - lag]
and upserts, so the cost is proportional to new data, not total data
(§8.3). Late samples below the watermark mark their bucket dirty in
bg_agg_invalid and the next refresh recomputes it (§8.4).
Known v0.1 limitation: sum_dt/sum_v_dt (time-weighted average
state) are computed from intervals within the refreshed window only —
the interval between the last sample before a window and its first
sample inside is not counted.
Summary
Types
Aggregate definition, one row of bg_agg_meta.
Functions
Creates the rollup table bg_agg_<name> and registers the aggregate
in bg_agg_meta. The watermark starts at zero, so the first refresh
backfills all existing data up to now - lag (Timescale behaviour).
All registered aggregate definitions.
Marks dirty buckets for late-arriving samples (spec §8.4): one row in
bg_agg_invalid per affected aggregate and bucket. rows are
{series_id, ts} pairs from a just-committed batch.
Refreshes one aggregate: recomputes invalidated buckets below the
watermark, then finalises buckets up to now - lag. Idempotent and
crash-safe — rerunning after a failure redoes the same work.
Types
@type definition() :: %{ name: String.t(), source: String.t(), bucket_width: pos_integer(), watermark: integer(), lag: non_neg_integer(), refresh_every: pos_integer() }
Aggregate definition, one row of bg_agg_meta.
bucket_width, watermark, and lag are in the database's time
unit; refresh_every is in milliseconds (it is a scheduling
interval, not a timestamp).
Functions
@spec create(Exqlite.Sqlite3.db(), String.t(), keyword(), atom()) :: :ok | {:error, term()}
Creates the rollup table bg_agg_<name> and registers the aggregate
in bg_agg_meta. The watermark starts at zero, so the first refresh
backfills all existing data up to now - lag (Timescale behaviour).
@spec definitions(Exqlite.Sqlite3.db()) :: [definition()]
All registered aggregate definitions.
@spec mark_invalidations(Exqlite.Sqlite3.db(), [{integer(), integer()}]) :: :ok
Marks dirty buckets for late-arriving samples (spec §8.4): one row in
bg_agg_invalid per affected aggregate and bucket. rows are
{series_id, ts} pairs from a just-committed batch.
@spec refresh(Exqlite.Sqlite3.db(), definition(), integer()) :: :ok
Refreshes one aggregate: recomputes invalidated buckets below the
watermark, then finalises buckets up to now - lag. Idempotent and
crash-safe — rerunning after a failure redoes the same work.
The aggregated range is [watermark, upper) — exclusive at the top,
so a sample exactly at upper stays in its (not yet complete) bucket
and is picked up by a later refresh.