src/sydra/query/validator.zig
Purpose
Performs semantic checks over the AST and produces diagnostics.
This stage is used by exec.execute before planning.
See also
Definition index (public)
pub const AnalyzeResult
diagnostics: DiagnosticListis_valid: bool
pub const AnalyzeError
Alias:
std.mem.Allocator.Error
pub const Analyzer
Methods:
Analyzer.init(allocator)analyze(statement)→AnalyzeResultdeinit(result)– frees diagnostic messages and list storage
Key validations (as implemented)
Time predicate requirement
DELETE always requires a time predicate. SELECT requires one only when a selector is present:
- If
SELECThas a selector andWHEREis missing, the analyzer emitstime_range_required. - If
SELECThas a selector andWHEREexists but does not referencetime, the analyzer emitstime_range_required. SELECTwithout a selector does not require a time predicate, but expressions are still validated.
The analyzer detects “time” references by scanning identifiers and treating an identifier as “time” when its trailing segment (after the last .) equals time case-insensitively.
Function name validation
For any call expression, the analyzer checks:
functions.lookup(call.callee.value) != null
Unknown functions emit invalid_syntax with a message like unknown function 'foo'.
Internal structure (non-public)
Important internal helpers in the implementation:
validateStatementdispatches onast.Statementvariants.validateSelectenforces the time predicate and walks projections/groupings/fill/order expressions.validateInsertwalksVALUES (...)expressions.validateDeleteenforces the time predicate and walks the predicate.visitExpressionwalks expressions and returns a boolean indicating whether the expression referencestime.- Span helpers:
exprSpan,exprStart,exprEndcompute spans for nested expressions.
Time detection:
- An identifier is treated as “time” when its trailing segment (after
.) equalstimecase-insensitively.
Code excerpt
src/sydra/query/validator.zig (time predicate + unknown function check excerpt)
pub fn analyze(self: *Analyzer, statement: *ast.Statement) AnalyzeError!AnalyzeResult {
var result = AnalyzeResult{};
try self.validateStatement(statement, &result);
result.is_valid = result.diagnostics.items.len == 0;
return result;
}
fn validateSelect(self: *Analyzer, select: *const ast.Select, result: *AnalyzeResult) AnalyzeError!void {
const requires_time_predicate = select.selector != null;
if (select.predicate) |pred| {
const predicate_has_time = try self.visitExpression(pred, result);
if (requires_time_predicate and !predicate_has_time) {
try self.addDiagnostic(result, .time_range_required, "time range predicate is required", exprSpan(pred));
}
} else if (requires_time_predicate) {
try self.addDiagnostic(result, .time_range_required, "select requires time predicate", select.span);
}
for (select.projections) |proj| {
_ = try self.visitExpression(proj.expr, result);
}
}
fn visitExpression(self: *Analyzer, expr: *const ast.Expr, result: *AnalyzeResult) AnalyzeError!bool {
return switch (expr.*) {
.identifier => |ident| {
return identifierIsTime(ident);
},
.literal => |literal| {
_ = literal;
return false;
},
.unary => |unary| {
return try self.visitExpression(unary.operand, result);
},
.binary => |binary| {
const left = try self.visitExpression(binary.left, result);
const right = try self.visitExpression(binary.right, result);
return left or right;
},
.call => |call| {
var has_time = false;
for (call.args) |arg| {
if (try self.visitExpression(arg, result)) {
has_time = true;
}
}
if (functions.lookup(call.callee.value) == null) {
const msg = try std.fmt.allocPrint(self.allocator, "unknown function '{s}'", .{call.callee.value});
defer self.allocator.free(msg);
try self.addDiagnostic(result, .invalid_syntax, msg, call.span);
}
return has_time;
},
};
}