src/sydra/query/physical.zig
Purpose
Lowers a logical plan into a physical plan with execution-oriented metadata.
The physical plan carries hints such as extracted time bounds for scan pushdown.
See also
- Logical plan builder and Optimizer
- Operator pipeline
- Engine (scan delegates to
Engine.queryRange)
Definition index (public)
pub const BuildError
Alias:
std.mem.Allocator.Error
pub const PhysicalPlan
root: *Node
pub const Node = union(enum)
Physical node kinds:
scanone_rowfilterprojectaggregatesortlimit
pub fn build(allocator, logical_root) !PhysicalPlan
Recursively transforms logical nodes into physical nodes, propagating context (notably time bounds).
pub fn nodeOutput(node: *Node) []const plan.ColumnInfo
Returns the output schema for a physical node.
Node payload structs (public)
Physical nodes are “execution oriented” versions of logical nodes:
Scan:selector: ?ast.Selectoroutput: []const plan.ColumnInforollup_hint: ?plan.RollupHinttime_bounds: TimeBounds
OneRow:output: []const plan.ColumnInfo— empty schema seed for constantSELECT
Filter:predicate: *const ast.Exproutput: []const plan.ColumnInfochild: *Nodeconjunction_count: usize— number of flattened conjuncts in the logical filtertime_bounds: TimeBounds— bounds extracted from the filter conjuncts
Project:columns: []const plan.ColumnInfochild: *Nodereuse_child_schema: bool— true when the child is also a project
Aggregate:groupings: []const ast.GroupExprrollup_hint: ?plan.RollupHintoutput: []const plan.ColumnInfochild: *Noderequires_hash: bool— true whenGROUP BYexistshas_fill_clause: bool— true when a fill clause exists
Sort:ordering: []const ast.OrderExprchild: *Nodeis_stable: bool— currently alwaystrueoutput: []const plan.ColumnInfo
Limit:limit: ast.LimitClausechild: *Nodeoffset: usize—limit.offset orelse 0output: []const plan.ColumnInfo
pub const TimeBounds = struct { ... }
Represents extracted time constraints:
min: ?i64/min_inclusive: boolmax: ?i64/max_inclusive: bool
Time bounds extraction (as implemented)
TimeBounds is derived from filter conjunctive predicates when it recognizes comparisons between:
- an identifier whose name is
time(case-insensitive), and - an integer literal
Supported operators include:
>=,>,<=,<,=
timeBoundsFromExpr (excerpt)
fn timeBoundsFromExpr(expr: *const ast.Expr) ?TimeBounds {
if (expr.* != .binary) return null;
const bin = expr.binary;
const lhs_time = exprIsTimeIdentifier(bin.left);
const rhs_time = exprIsTimeIdentifier(bin.right);
if (!lhs_time and !rhs_time) return null;
const op = bin.op;
if (lhs_time and rhs_time) return null;
const literal = if (lhs_time) convertTimeLiteral(bin.right) else convertTimeLiteral(bin.left);
if (literal == null) return null;
const value = literal.?;
var bounds = TimeBounds{};
if (lhs_time) {
switch (op) {
.greater_equal => { bounds.min = value; bounds.min_inclusive = true; },
.greater => { bounds.min = value; bounds.min_inclusive = false; },
.less_equal => { bounds.max = value; bounds.max_inclusive = true; },
.less => { bounds.max = value; bounds.max_inclusive = false; },
.equal => { bounds.min = value; bounds.max = value; },
else => return null,
}
} else {
// time on right side flips the bounds direction
// ...
}
return bounds;
}
Extracted bounds are merged and propagated down into the scan node to constrain Engine.queryRange.
Important notes:
- Only the identifier value
timeis recognized (it does not matchtag.timeor other dotted identifiers). - Only integer literals are accepted for bound extraction (no floats, strings, or
now()yet). - Bounds are merged across conjuncts using “tightest wins” semantics:
- higher minimum replaces lower minimum
- lower maximum replaces higher maximum
- equal bounds combine inclusivity