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
SELECT and DELETE require a time predicate:
- If
WHEREis missing, the analyzer emitstime_range_required. - If
WHEREexists but does not referencetime, the analyzer emitstime_range_required.
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 {
var predicate_has_time = false;
if (select.predicate) |pred| {
predicate_has_time = try self.visitExpression(pred, result);
if (!predicate_has_time) {
try self.addDiagnostic(result, .time_range_required, "time range predicate is required", exprSpan(pred));
}
} else {
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;
},
};
}