Skip to main content
Version: 0.1.33

Calculated Fields

A calculated field is a column the pipeline computes from the row's other columns and writes back onto the same table — next to the raw data, not into a separate mirror. The classic example: a vehicle whose speed is 0 while rpm is climbing is at idle, so kontak (ignition) is 1 — computed on the raw row itself, for every table kind.

SELECT speed, rpm, kontak FROM vehicle_events; -- raw and computed, side by side

The field belongs to the table

Each definition lives as data in the database's x_calc_columns table — one row per (target_table, column), versioned, enabled or not. The pipeline that computes a table is always the same three nodes, sink pointed back at the source:

TableTrigger(vehicle_events) → CalcColumns(vehicle_events) → TableSink(vehicle_events)

CalcColumns fetches the target's definitions fresh at every run — editing one is a hot reload. No republish, no restart: the next data that arrives is computed the new way.

The column is born the moment you define it, carrying the CALCULATED attribute. That attribute changes one thing in the engine: when the pipeline rewrites a computed value at a coordinate it already wrote, the read resolves to the newest write — a recompute is a correction, not a second observation. Raw columns are untouched; every differing sample is still kept.

A definition is name + type + triggers + events + script

partmeaning
namethe column the field writes (reserved prefixes x_, _ refused)
typebool / int / float64 / string — the column is born with it, and every result is checked against it (a mismatch is dropped, not written)
triggersrun only when one of these columns arrives (raw) or changes (a computed sibling); empty = every row
on eventsrun only on insert / update / delete; empty = all
scriptthe Lua below

Writing the script

Define compute(row, prev, event) and return the column's value. You may declare your own helper functions and constants alongside it:

local function band(v)
if v < 1000 then return 'low' elseif v < 2000 then return 'mid' else return 'high' end
end

function compute(row, prev, event)
return band(row.rpm)
end

For the simple case a bare function body works too — it is wrapped into compute for you:

return row.speed == 0 and row.rpm > 0

Each field's script loads in its own closure, so helpers stay private and never collide between fields.

row — the current row

row carries every source column (row.entity, row.ts, row.speed, …) and every calculated sibling your script reads. Fields are scheduled by their row.<col> references, so a field reading row.p00_dev runs after p00_dev, however the names sort. Circular references are refused by name. A missing value is nil (sparse fragments are normal) — guard with or.

prev — the previous row

prev holds this entity's previous row (its trigger and computed columns), so change over time is a subtraction:

function compute(row, prev, event)
if event == 'insert' then return false end
return row.rpm > (prev.rpm or 0) -- "rising"
end

prev and event advance only as the entity moves forward in time (rev). A replay, or the pipeline's own feed echo, is left untouched — so a calculated value never flickers and the loop converges.

event — what happened

event is "insert" (the first time this entity is seen), "update" (later), or "delete" (a soft-deleted rows-table row). Gate a field to specific events with on events, or branch on it in the script.

state — persistent memory

A script that reads the global state gets a per-entity memory: a Lua table keyed by the row's identity (entity for timeseries, xid for rows), alive across the chunk, loaded from the x_calc_state kv store before the run and persisted after — a running EWMA survives runs, restarts and daemon failover:

function compute(row, prev, event)
state.v = (state.v or row.p00) * 0.9 + row.p00 * 0.1
return state.v
end

Keys beginning __ are reserved (that is where prev is kept). At-least-once caveat: if a run fails after the state advanced, the replayed rows are seen by the state twice.

Scripts run in the runner's Lua sandbox: no filesystem, no network, a hard time and memory ceiling, in a throwaway child process. The worst a field can do is fail its own run.

The console

Under each table the sidebar has a Calculated Fields section: its menu offers New calculated field, and each field carries an Edit / Delete menu (delete is confirmed and removes only the formula — the column and its computed values stay). The editor is a workspace tab: field name (renamable), type, triggers, on-events, a Lua editor with a Preview that runs your script in the browser, and an honest status line naming the pipeline that computes this table. A computed column wears an fx badge in the Fields list.

Everything the console does is plain SQL on x_calc_columns, so automation can do the same:

INSERT INTO x_calc_columns
(target_table, col, script, version, enabled, type, trigger_cols, on_events)
VALUES ('vehicle_events', 'kontak',
'return row.speed == 0 and row.rpm > 0',
1, true, 'bool', '["speed","rpm"]', NULL);

Durability and the past

Column births follow the database's durability mode (see Durability). A saved formula applies to data from now on — rows computed under the old version stay as they were, often exactly what you want. To recompute history, empty the table and clear the pipeline's cursor:

TRUNCATE TABLE vehicle_events;
DELETE FROM x_re_cursors WHERE key = 'vehicle-calc/vehicle_events';

— and the trigger replays from zero through the current formulas on its next wakeup. (Truncating a synced table is louder: its epoch bump refuses the old cursor and forces the same full replay on its own.)