src/sydra/query/plan.zig
Purpose
Builds a logical plan from the sydraQL AST (ast.zig).
Logical plans are later optimized and lowered into a physical plan.
See also
- Validator (ensures the AST is semantically valid)
- Optimizer
- Physical plan builder
- Operator pipeline
Definition index (public)
pub const BuildError = error { ... }
UnsupportedStatement— currently onlySELECTis supported by the logical planner
pub const Node = union(enum)
Logical node kinds:
scanone_rowfilterprojectaggregatesortlimit
pub const ColumnInfo = struct { ... }
name: []const u8expr: *const ast.Expr
This is the “schema” that flows through planning and execution.
pub const RollupHint = struct { ... }
bucket_expr: *const ast.Expr— currently used to marktime_bucket(...)grouping expressions
pub fn nodeOutput(node: *Node) []const ColumnInfo
Returns the output schema for a node.
pub const Builder
Fields:
allocator: std.mem.Allocatorcolumn_counter: usize = 0— used to generate stable column names across multiple SELECT lists
Key methods:
Builder.init(allocator)build(statement)– currently supportsSELECTonly
Key behaviors (as implemented)
- Builds a default scan schema of
timeandvalueidentifiers. - Splits
WHERE a AND b AND cintoFilter.conjunctive_predicates. - Determines whether aggregation is needed if:
- any
GROUP BYexists, or - any projection contains an aggregate/window function (via the function registry).
- any
- Detects a rollup hint when
GROUP BYincludestime_bucket(...). - Projection column naming:
- Uses explicit aliases when present
- Otherwise uses identifier name, or
fnName_<n>, or_col<n>
Node payload structs (public)
Each Node tag has a corresponding payload struct:
Scan:source: *const ast.Selectselector: ?ast.Selectoroutput: []const ColumnInfo
OneRow:output: []const ColumnInfo— empty schema seed for constantSELECT
Filter:input: *Nodepredicate: *const ast.Expr— combined predicate (may re-build an AND chain)output: []const ColumnInfoconjunctive_predicates: []const *const ast.Expr— flattenedANDclauses from the WHERE predicate
Project:input: *Nodeprojections: []const ast.Projectionoutput: []const ColumnInfo
Aggregate:input: *Nodegroupings: []const ast.GroupExprprojections: []const ast.Projectionfill: ?ast.FillClauserollup_hint: ?RollupHintoutput: []const ColumnInfo
Sort:input: *Nodeordering: []const ast.OrderExproutput: []const ColumnInfo
Limit:input: *Nodelimit: ast.LimitClauseoutput: []const ColumnInfo
Internal helpers (non-public)
Important builder helpers in the implementation:
buildSelect— constructs the node chain in this order:one_row(no selector) orscan(selector present)- optional
filter→ optionalaggregate→project→ optionalsort→ optionallimit
collectPredicates— flattensa AND bbinary expressions into a slicecombinePredicates— rebuilds a single AND-chain expression and unions spansinferProjectionName— generates output column names when no alias is provideddefaultScanColumns— creates synthetic identifier expressions fortimeandvalue
Tests
Inline tests cover simple select planning, conjunctive predicates, rollup hints, and alias retention.
Code excerpt
src/sydra/query/plan.zig (node chain + buildSelect excerpt)
pub const Node = union(enum) {
scan: Scan,
one_row: OneRow,
filter: Filter,
project: Project,
aggregate: Aggregate,
sort: Sort,
limit: Limit,
};
fn buildSelect(self: *Builder, select: *const ast.Select) (BuildError || std.mem.Allocator.Error)!*Node {
const projection_columns = try self.buildColumns(select.projections);
self.column_counter += projection_columns.len;
var current: *Node = undefined;
if (select.selector == null) {
current = try self.makeNode(.{
.one_row = .{
.output = empty_columns[0..],
},
});
} else {
const scan_columns = try self.defaultScanColumns();
current = try self.makeNode(.{
.scan = .{
.source = select,
.selector = select.selector,
.output = scan_columns,
},
});
}
var filter_list = ManagedArrayList(*const ast.Expr).init(self.allocator);
try self.collectPredicates(select.predicate, &filter_list);
var filter_conditions: []const *const ast.Expr = &.{};
if (filter_list.items.len != 0) {
filter_conditions = try filter_list.toOwnedSlice();
const predicate = try self.combinePredicates(filter_conditions);
current = try self.makeNode(.{
.filter = .{
.input = current,
.predicate = predicate,
.output = nodeOutput(current),
.conjunctive_predicates = filter_conditions,
},
});
}
filter_list.deinit();
if (needsAggregation(select)) {
const rollup_hint = detectRollupHint(select.groupings);
current = try self.makeNode(.{
.aggregate = .{
.input = current,
.groupings = select.groupings,
.projections = select.projections,
.fill = select.fill,
.rollup_hint = rollup_hint,
.output = projection_columns,
},
});
}
current = try self.makeNode(.{
.project = .{
.input = current,
.projections = select.projections,
.output = projection_columns,
},
});
return current;
}