Query Engine
The query engine resolves channel selections and evaluates time-series expressions against the silver-layer data. It has two parts:
- TSAL — the expression language you write to select channels, derive virtual signals, and define events and aggregations.
- Query Solvers — the
DefaultSolverthat knows how your silver tables are laid out, reads them, and evaluates the expressions per container.
Two ways to use it
-
Directly, for ad-hoc analysis. Build a query against a
MeasurementDBand solve it interactively — no reporting setup required:eng_rpm = db.query.channel(channel_name='Engine RPM')
result = db.query.select(eng_rpm.mean().alias('rpm_mean')).solve(spark, solver=DefaultSolver(spark))
# or .toPandas(spark, solver=DefaultSolver(spark)) for a pandas DataFrame -
As the foundation of the Report component. The reporting layer embeds TSAL expressions in events and aggregations, and the
Reportorchestrator batch-solves them through the query engine and persists the results to the gold layer.
Snapshot consistency
A single solve fans out into many lazy reads across the silver tables. By default each read sees the latest Delta version at the moment it materializes, so a table that changes mid-analysis can be observed inconsistently across those reads.
To freeze a consistent snapshot, pin the silver tables to their current Delta versions before
querying — every subsequent read then uses versionAsOf that pinned version:
db.pin_versions(spark) # resolve each table's current version once
result = db.query.select(...).solve(spark, solver=DefaultSolver(spark))
For direct query-engine use, pinning is opt-in — call pin_versions yourself when you want it.
The pin is stored on the config and persists until cleared, so a long-lived MeasurementDB keeps
reading the pinned snapshot until you re-pin (call it again) or unpin (db.config.pinned_versions = {}).
The Report component pins automatically at the start of every run, so snapshot
consistency there needs no action.