Skip to main content

reddb_rql/
ast.rs

1//! Unified Query AST
2//!
3//! Defines the abstract syntax tree for unified table+graph queries.
4//! Supports:
5//! - Pure table queries (SELECT ... FROM ...)
6//! - Pure graph queries (MATCH (a)-[r]->(b) ...)
7//! - Table-graph joins (FROM t JOIN GRAPH ...)
8//! - Path queries (PATH FROM ... TO ... VIA ...)
9//!
10//! # Examples
11//!
12//! ```text
13//! -- Table query
14//! SELECT ip, ports FROM hosts WHERE os = 'Linux'
15//!
16//! -- Graph query
17//! MATCH (h:Host)-[:HAS_SERVICE]->(s:Service)
18//! WHERE h.ip STARTS WITH '192.168'
19//! RETURN h, s
20//!
21//! -- Join query
22//! FROM hosts h
23//! JOIN GRAPH (h)-[:HAS_VULN]->(v:Vulnerability) AS g
24//! WHERE h.criticality > 7
25//! RETURN h.ip, h.hostname, v.cve
26//!
27//! -- Path query
28//! PATH FROM host('192.168.1.1') TO host('10.0.0.1')
29//! VIA [:AUTH_ACCESS, :CONNECTS_TO]
30//! RETURN path
31//! ```
32
33#[path = "builders.rs"]
34mod builders;
35#[path = "core.rs"]
36mod core;
37#[path = "geo_predicate.rs"]
38pub mod geo_predicate;
39
40pub use builders::*;
41pub use core::*;
42
43#[cfg(test)]
44#[path = "ast_tests.rs"]
45mod tests;
46
47// ============================================================================
48// Fase 2 — Expression AST (Week 1 foundation)
49//
50// Types below are the foundation for the parser v2 rewrite described in
51// `/home/cyber/.claude/plans/squishy-mixing-honey.md` (Fase 2). They are
52// additive — existing `Filter`, `Projection`, `OrderByClause`, and
53// `TableQuery` keep working unchanged. Future weeks migrate those AST
54// slots to carry an `Expr` instead of ad-hoc `FieldRef` / `Value` /
55// `String` fields so deferred Fase 1 items (1.6 ORDER BY expression,
56// 1.7 FROM (SELECT …)) can land without further AST churn.
57//
58// Design notes:
59// - `Expr` is an *untyped* syntactic tree. Semantic resolution — type
60//   inference, name resolution, coercion pathway — happens in the
61//   `analyze/` pass once it exists (Week 2-3 of Fase 2).
62// - `Span` uses the existing `lexer::Position` so errors can point at
63//   the original source range without re-tokenising.
64// - `BinOp` is a flat enum; precedence lives in the parser, not here.
65// ============================================================================
66
67use crate::lexer::Position;
68use reddb_types::types::{DataType, Value};
69
70/// Half-open byte / line / column range into the original input string.
71/// Both endpoints come from the lexer so downstream passes can re-open
72/// the source and print a caret-pointed diagnostic without re-lexing.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
74pub struct Span {
75    pub start: Position,
76    pub end: Position,
77}
78
79impl Span {
80    pub fn new(start: Position, end: Position) -> Self {
81        Self { start, end }
82    }
83
84    /// A synthetic span marker used when a node is constructed
85    /// programmatically rather than parsed from source. Debug diagnostics
86    /// should check for this via `is_synthetic()` and suppress location
87    /// pointers rather than printing `0:0`.
88    pub fn synthetic() -> Self {
89        Self::default()
90    }
91
92    pub fn is_synthetic(&self) -> bool {
93        self.start == Position::default() && self.end == Position::default()
94    }
95}
96
97/// Syntactic binary operators — the operator vocabulary.
98///
99/// `BinOp` was re-homed into the neutral keystone crate
100/// [`reddb_types::operator`] (ADR 0052): the coercion spine that keys
101/// overload resolution on it now lives there, and leaving the enum behind
102/// would force that crate to depend back on `reddb-server`. This re-export
103/// keeps the historical `ast::BinOp` path resolving so every parser /
104/// analyzer / evaluator call-site stays untouched.
105pub use reddb_types::operator::BinOp;
106
107/// Unary operators — only the two real unaries SQL actually has.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum UnaryOp {
110    /// Arithmetic negation: `-expr`
111    Neg,
112    /// Logical negation: `NOT expr`
113    Not,
114}
115
116/// The syntactic expression tree. Every node carries a `Span` so
117/// semantic errors from the analyze pass can point back at the exact
118/// token range. Created by the Fase 2 parser, consumed by the analyzer
119/// and (eventually) the planner.
120#[derive(Debug, Clone, PartialEq)]
121pub enum Expr {
122    /// A literal value (number, string, boolean, null).
123    Literal { value: Value, span: Span },
124    /// Reference to a column (possibly qualified by table / alias).
125    Column { field: FieldRef, span: Span },
126    /// Query parameter placeholder (`?` or `$n`). Used by prepared
127    /// statements in Fase 4 — the plan cache strips these so repeated
128    /// bindings reuse the same plan.
129    Parameter { index: usize, span: Span },
130    /// Binary infix operator: `lhs <op> rhs`.
131    BinaryOp {
132        op: BinOp,
133        lhs: Box<Expr>,
134        rhs: Box<Expr>,
135        span: Span,
136    },
137    /// Prefix unary operator.
138    UnaryOp {
139        op: UnaryOp,
140        operand: Box<Expr>,
141        span: Span,
142    },
143    /// `CAST(expr AS type)` / `expr::type`.
144    Cast {
145        inner: Box<Expr>,
146        target: DataType,
147        span: Span,
148    },
149    /// Function / aggregate call.
150    FunctionCall {
151        name: String,
152        args: Vec<Expr>,
153        span: Span,
154    },
155    /// `CASE WHEN cond THEN val [...] [ELSE val] END`.
156    Case {
157        branches: Vec<(Expr, Expr)>,
158        else_: Option<Box<Expr>>,
159        span: Span,
160    },
161    /// `IS NULL` / `IS NOT NULL`. Kept as a distinct variant because
162    /// SQL treats them as unary postfix operators with special
163    /// three-valued semantics.
164    IsNull {
165        operand: Box<Expr>,
166        negated: bool,
167        span: Span,
168    },
169    /// `expr IN (v1, v2, …)`. The rhs list is `Vec<Expr>` — at Week 1
170    /// only literal lists survive analyze; correlated subquery lists
171    /// land in Week 3 alongside the `Subquery` variant below.
172    InList {
173        target: Box<Expr>,
174        values: Vec<Expr>,
175        negated: bool,
176        span: Span,
177    },
178    /// `expr BETWEEN low AND high` — first-class so pushdown can
179    /// recognise range predicates without decomposing to `>=` and `<=`.
180    Between {
181        target: Box<Expr>,
182        low: Box<Expr>,
183        high: Box<Expr>,
184        negated: bool,
185        span: Span,
186    },
187    /// Parenthesized SELECT used in an expression context.
188    Subquery { query: ExprSubquery, span: Span },
189    /// Window function call: `fn(args) OVER (PARTITION BY ... ORDER BY
190    /// ... [frame])`. Carries the same `name`/`args` payload as a plain
191    /// `FunctionCall` plus a `WindowSpec` describing the OVER clause.
192    /// Issue #589 slice 7a — parser + AST only; no runtime execution.
193    WindowFunctionCall {
194        name: String,
195        args: Vec<Expr>,
196        window: WindowSpec,
197        span: Span,
198    },
199}
200
201#[derive(Debug, Clone)]
202pub struct ExprSubquery {
203    pub query: Box<QueryExpr>,
204}
205
206impl PartialEq for ExprSubquery {
207    fn eq(&self, other: &Self) -> bool {
208        format!("{:?}", self.query) == format!("{:?}", other.query)
209    }
210}
211
212impl Expr {
213    /// Extract the span of this expression. Synthetic nodes return
214    /// `Span::synthetic()` — callers that need a real location should
215    /// check `span.is_synthetic()` before rendering diagnostics.
216    pub fn span(&self) -> Span {
217        match self {
218            Expr::Literal { span, .. }
219            | Expr::Column { span, .. }
220            | Expr::Parameter { span, .. }
221            | Expr::BinaryOp { span, .. }
222            | Expr::UnaryOp { span, .. }
223            | Expr::Cast { span, .. }
224            | Expr::FunctionCall { span, .. }
225            | Expr::Case { span, .. }
226            | Expr::IsNull { span, .. }
227            | Expr::InList { span, .. }
228            | Expr::Between { span, .. }
229            | Expr::Subquery { span, .. }
230            | Expr::WindowFunctionCall { span, .. } => *span,
231        }
232    }
233
234    /// Constructor shortcut for the common `Literal` case.
235    pub fn lit(value: Value) -> Self {
236        Expr::Literal {
237            value,
238            span: Span::synthetic(),
239        }
240    }
241
242    /// Constructor shortcut for the common `Column` case.
243    pub fn col(field: FieldRef) -> Self {
244        Expr::Column {
245            field,
246            span: Span::synthetic(),
247        }
248    }
249
250    /// Convenience: build a binary operation with a synthetic span.
251    /// Used by unit tests and by the Projection → Expr shim while the
252    /// migration is in flight.
253    pub fn binop(op: BinOp, lhs: Expr, rhs: Expr) -> Self {
254        Expr::BinaryOp {
255            op,
256            lhs: Box::new(lhs),
257            rhs: Box::new(rhs),
258            span: Span::synthetic(),
259        }
260    }
261}
262
263#[cfg(test)]
264mod expr_tests {
265    use super::*;
266
267    #[test]
268    fn precedence_orders_mul_over_add_and_and_over_or() {
269        // Higher precedence binds tighter — classic `a OR b AND c` trap.
270        assert!(BinOp::Mul.precedence() > BinOp::Add.precedence());
271        assert!(BinOp::Add.precedence() > BinOp::Eq.precedence());
272        assert!(BinOp::Eq.precedence() > BinOp::And.precedence());
273        assert!(BinOp::And.precedence() > BinOp::Or.precedence());
274    }
275
276    #[test]
277    fn span_synthetic_round_trip() {
278        let s = Span::synthetic();
279        assert!(s.is_synthetic());
280        let real = Span::new(Position::new(1, 1, 0), Position::new(1, 5, 4));
281        assert!(!real.is_synthetic());
282    }
283
284    #[test]
285    fn expr_constructors_carry_synthetic_span() {
286        let lit = Expr::lit(Value::Integer(42));
287        assert!(lit.span().is_synthetic());
288        assert_eq!(
289            lit,
290            Expr::Literal {
291                value: Value::Integer(42),
292                span: Span::synthetic(),
293            }
294        );
295    }
296
297    #[test]
298    fn binop_shortcut_nests() {
299        // a + b * c parses to Add(a, Mul(b, c)) under normal precedence
300        let expr = Expr::binop(
301            BinOp::Add,
302            Expr::col(FieldRef::column("", "a")),
303            Expr::binop(
304                BinOp::Mul,
305                Expr::col(FieldRef::column("", "b")),
306                Expr::col(FieldRef::column("", "c")),
307            ),
308        );
309        match expr {
310            Expr::BinaryOp {
311                op: BinOp::Add,
312                rhs,
313                ..
314            } => match *rhs {
315                Expr::BinaryOp { op: BinOp::Mul, .. } => {}
316                other => panic!("expected Mul on rhs, got {:?}", other),
317            },
318            other => panic!("expected Add at root, got {:?}", other),
319        }
320    }
321}