Skip to main content

synadb/query/
ast.rs

1//! Abstract syntax tree for Syna Query.
2//!
3//! Covers both EQL (SQL-like) and EMQ (MongoDB-like) syntaxes. The parsers
4//! in later tasks produce [`QueryAst`] values that the planner/executor
5//! consume.
6
7use crate::types::Atom;
8use serde::{Deserialize, Serialize};
9use std::fmt;
10
11// ═══════════════════════════════════════════════════════════════════════
12//  Root AST
13// ═══════════════════════════════════════════════════════════════════════
14
15/// Root AST node for every query type Syna Query understands.
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17pub enum QueryAst {
18    /// SQL-like SELECT statement.
19    Select(SelectQuery),
20    /// MongoDB-like find document.
21    Find(FindQuery),
22    /// Aggregation query (`SELECT AVG(value) FROM ...`).
23    Aggregate(AggregateQuery),
24    /// Temporal join query.
25    TemporalJoin(TemporalJoinQuery),
26    /// Streaming (continuous) query.
27    Stream(StreamQuery),
28    /// EXPLAIN or EXPLAIN ANALYZE wrapping another query.
29    Explain(Box<QueryAst>),
30    /// Macro definition (CREATE MACRO ...).
31    Macro(MacroDefinition),
32    /// Data lineage query (LINEAGE() / DERIVED_FROM()).
33    Lineage(LineageQuery),
34}
35
36// ═══════════════════════════════════════════════════════════════════════
37//  EQL SELECT
38// ═══════════════════════════════════════════════════════════════════════
39
40/// A SQL-like SELECT query.
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42pub struct SelectQuery {
43    /// Columns/expressions to project.
44    pub projections: Vec<Projection>,
45    /// FROM clause — which keys to scan.
46    pub from: Option<KeyPattern>,
47    /// WHERE clause.
48    pub where_clause: Option<WhereClause>,
49    /// ORDER BY clause.
50    pub order_by: Option<OrderBy>,
51    /// LIMIT N.
52    pub limit: Option<u64>,
53    /// OFFSET N.
54    pub offset: Option<u64>,
55}
56
57// ═══════════════════════════════════════════════════════════════════════
58//  EMQ Find
59// ═══════════════════════════════════════════════════════════════════════
60
61/// A MongoDB-like find query built from a JSON filter document.
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63pub struct FindQuery {
64    /// Filter document (`{ value: { $gt: 10 } }`).
65    pub filter: FilterDocument,
66    /// Optional projection document (`{ key: 1, value: 1 }`).
67    pub projection: Option<ProjectionDocument>,
68    /// Optional sort document (`{ timestamp: -1 }`).
69    pub sort: Option<SortDocument>,
70    /// Upper bound on results.
71    pub limit: Option<u64>,
72    /// How many matching documents to skip.
73    pub skip: Option<u64>,
74}
75
76/// Placeholder for EMQ filter documents. Populated by the EMQ parser.
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
78pub struct FilterDocument {
79    /// Raw JSON representation of the filter.
80    pub raw: serde_json::Value,
81}
82
83/// Placeholder for EMQ projection documents.
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
85pub struct ProjectionDocument {
86    /// Raw JSON representation of the projection.
87    pub raw: serde_json::Value,
88}
89
90/// Placeholder for EMQ sort documents.
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
92pub struct SortDocument {
93    /// Raw JSON representation of the sort spec.
94    pub raw: serde_json::Value,
95}
96
97// ═══════════════════════════════════════════════════════════════════════
98//  Aggregate
99// ═══════════════════════════════════════════════════════════════════════
100
101/// An aggregation query (`SELECT AVG(value) FROM ... GROUP BY ...`).
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103pub struct AggregateQuery {
104    /// Aggregate functions to compute.
105    pub aggregations: Vec<AggregateFunction>,
106    /// FROM clause.
107    pub from: Option<KeyPattern>,
108    /// WHERE clause applied before aggregation.
109    pub where_clause: Option<WhereClause>,
110    /// Optional GROUP BY.
111    pub group_by: Option<GroupBy>,
112    /// Optional HAVING clause.
113    pub having: Option<WhereClause>,
114    /// ORDER BY.
115    pub order_by: Option<OrderBy>,
116    /// LIMIT.
117    pub limit: Option<u64>,
118}
119
120/// Available aggregate functions.
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
122pub enum AggregateFunction {
123    /// `COUNT(*)`.
124    Count,
125    /// `SUM(value)` — numeric only.
126    Sum,
127    /// `AVG(value)` — numeric only.
128    Avg,
129    /// `MIN(value)`.
130    Min,
131    /// `MAX(value)`.
132    Max,
133    /// `FIRST(value)` — earliest value.
134    First,
135    /// `LAST(value)` — latest value.
136    Last,
137}
138
139/// GROUP BY specification.
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141pub enum GroupBy {
142    /// Group by key (each distinct key is a group).
143    Key,
144    /// Group by time bucket.
145    TimeBucket(TimeBucket),
146}
147
148/// Time bucket sizes for `GROUP BY TIME_BUCKET(...)`.
149#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
150pub enum TimeBucket {
151    /// 1 minute.
152    Minute,
153    /// 1 hour.
154    Hour,
155    /// 1 day.
156    Day,
157    /// 1 week.
158    Week,
159    /// 1 month.
160    Month,
161}
162
163// ═══════════════════════════════════════════════════════════════════════
164//  Temporal Join / Stream / Macro / Lineage (placeholders)
165// ═══════════════════════════════════════════════════════════════════════
166
167/// Temporal join query (`A TEMPORAL JOIN B ASOF WITHIN 5m`).
168///
169/// Placeholder populated by Task 12.
170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
171pub struct TemporalJoinQuery {
172    /// Left side key pattern.
173    pub left: KeyPattern,
174    /// Right side key pattern.
175    pub right: KeyPattern,
176    /// Join type (e.g., "ASOF", "INTERPOLATED", "FORWARD_FILL").
177    pub join_type: String,
178    /// Maximum time gap accepted for the match, in microseconds.
179    pub within_micros: Option<u64>,
180}
181
182/// Streaming (continuous) query.
183///
184/// Placeholder populated by Task 19.
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
186pub struct StreamQuery {
187    /// Stream name.
188    pub name: String,
189    /// Underlying query definition.
190    pub body: Box<QueryAst>,
191}
192
193/// Macro definition.
194///
195/// Placeholder populated by Task 21.
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
197pub struct MacroDefinition {
198    /// Macro name.
199    pub name: String,
200    /// Parameter names.
201    pub params: Vec<String>,
202    /// Body template (not yet parsed until expansion).
203    pub body: String,
204}
205
206/// Data lineage query.
207///
208/// Placeholder populated by Task 22.
209#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
210pub struct LineageQuery {
211    /// Key whose lineage is being queried.
212    pub key: String,
213}
214
215// ═══════════════════════════════════════════════════════════════════════
216//  Shared AST pieces — key patterns, conditions, projections
217// ═══════════════════════════════════════════════════════════════════════
218
219/// Key matching patterns.
220#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
221pub enum KeyPattern {
222    /// Exact key match (`"sensor/temp"`).
223    Exact(String),
224    /// Prefix match (`"sensor/"`).
225    Prefix(String),
226    /// Glob pattern (`"sensor/*"`).
227    Glob(String),
228    /// Regex pattern.
229    Regex(String),
230    /// Union of multiple patterns (`A OR B`).
231    Union(Vec<KeyPattern>),
232}
233
234/// Comparison operators used in value filters.
235#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
236pub enum ComparisonOp {
237    /// `=`, `==`, `$eq`.
238    Eq,
239    /// `!=`, `<>`, `$ne`.
240    Ne,
241    /// `>`, `$gt`.
242    Gt,
243    /// `>=`, `$gte`.
244    Gte,
245    /// `<`, `$lt`.
246    Lt,
247    /// `<=`, `$lte`.
248    Lte,
249    /// `IN`, `$in`.
250    In,
251    /// `NOT IN`, `$nin`.
252    Nin,
253    /// `LIKE`.
254    Like,
255    /// `REGEX`, `$regex`.
256    Regex,
257}
258
259/// Boolean composition of conditions.
260#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
261pub enum BooleanOp {
262    /// Conjunction.
263    And(Vec<Condition>),
264    /// Disjunction.
265    Or(Vec<Condition>),
266    /// Negation.
267    Not(Box<Condition>),
268}
269
270/// Which field of a row a condition or ordering applies to.
271#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
272pub enum OrderField {
273    /// The row key.
274    Key,
275    /// The stored value.
276    Value,
277    /// The write timestamp.
278    Timestamp,
279}
280
281/// Sort direction.
282#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
283pub enum Direction {
284    /// Ascending.
285    Asc,
286    /// Descending.
287    Desc,
288}
289
290/// An ORDER BY clause.
291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
292pub struct OrderBy {
293    /// Field to sort by.
294    pub field: OrderField,
295    /// Direction.
296    pub direction: Direction,
297}
298
299/// A time range filter for WHERE clauses.
300#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
301pub struct TimeRange {
302    /// Inclusive start, Unix microseconds.
303    pub start: Option<u64>,
304    /// Inclusive end, Unix microseconds.
305    pub end: Option<u64>,
306}
307
308/// One or more values used on the right-hand side of a comparison.
309#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
310pub enum ValueFilter {
311    /// A single atom.
312    Single(Atom),
313    /// A list of atoms (used for `IN` / `NOT IN`).
314    List(Vec<Atom>),
315}
316
317/// The WHERE clause root — a tree of conditions.
318#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
319pub struct WhereClause {
320    /// Root condition.
321    pub root: Condition,
322}
323
324/// A single predicate inside a WHERE clause.
325#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
326pub enum Condition {
327    /// `<field> <op> <value>`.
328    Comparison {
329        /// Left operand (field or timestamp reference).
330        field: OrderField,
331        /// Operator.
332        op: ComparisonOp,
333        /// Right-hand operand.
334        rhs: ValueFilter,
335    },
336    /// Key-pattern predicate (`FROM "sensor/*"` may also appear here in OR unions).
337    Key(KeyPattern),
338    /// `timestamp BETWEEN a AND b`.
339    TimeRange(TimeRange),
340    /// `ANOMALY(value, <method>)`.
341    Anomaly(AnomalyMethod),
342    /// `MATCHES_PATTERN(value, '<pattern>')`.
343    Pattern(TimeSeriesPattern),
344    /// `FRESHNESS <op> <num>` / `STALE` / `FRESH` (DAVO).
345    Freshness(FreshnessCondition),
346    /// `SIMILAR_TO(<vec>, <k>)`.
347    Similarity(SimilarityCondition),
348    /// `AND` / `OR` / `NOT`.
349    Boolean(BooleanOp),
350}
351
352// ─── WHERE-clause sub-types (placeholders for later tasks) ─────────────
353
354/// Anomaly-detection method used inside `ANOMALY(...)`.
355#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
356pub enum AnomalyMethod {
357    /// Z-score, with threshold.
358    ZScore(f64),
359    /// Interquartile range, with multiplier.
360    Iqr(f64),
361    /// Moving-average deviation.
362    MovingAverage {
363        /// Rolling window.
364        window: u64,
365        /// Threshold in std-devs.
366        threshold: f64,
367    },
368}
369
370/// Named time-series shape for `MATCHES_PATTERN`.
371#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
372pub enum TimeSeriesPattern {
373    /// Sudden rise then fall.
374    Spike {
375        /// Minimum amplitude.
376        threshold: f64,
377        /// Maximum duration in microseconds.
378        max_duration_micros: Option<u64>,
379    },
380    /// Sudden drop then recovery.
381    Dip {
382        /// Minimum amplitude.
383        threshold: f64,
384        /// Maximum duration in microseconds.
385        max_duration_micros: Option<u64>,
386    },
387    /// Sustained rising trend.
388    Rising {
389        /// Minimum duration in microseconds.
390        min_duration_micros: u64,
391    },
392    /// Sustained falling trend.
393    Falling {
394        /// Minimum duration in microseconds.
395        min_duration_micros: u64,
396    },
397    /// Flat region.
398    Plateau {
399        /// Minimum duration in microseconds.
400        min_duration_micros: u64,
401        /// Maximum variance allowed.
402        tolerance: f64,
403    },
404}
405
406/// DAVO freshness condition (Task 30).
407#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
408pub enum FreshnessCondition {
409    /// `FRESHNESS <op> <value>`.
410    Compare {
411        /// Operator.
412        op: ComparisonOp,
413        /// Threshold in \[0, 1\].
414        value: f64,
415    },
416    /// `STALE` — freshness below configured threshold.
417    Stale,
418    /// `FRESH` — freshness at or above configured threshold.
419    Fresh,
420}
421
422/// Vector similarity condition (Task 28).
423#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
424pub struct SimilarityCondition {
425    /// Query vector.
426    pub query_vector: Vec<f32>,
427    /// Number of neighbours requested.
428    pub k: usize,
429    /// Optional index hint (e.g., `"HNSW"`, `"GWI"`, `"CASCADE"`).
430    pub index_hint: Option<String>,
431}
432
433// ─── Projection & function calls ────────────────────────────────────────
434
435/// What to return in each result row.
436#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
437pub enum Projection {
438    /// `*`.
439    All,
440    /// `key`.
441    Key,
442    /// `value`.
443    Value,
444    /// `timestamp`.
445    Timestamp,
446    /// A function call like `AVG(value)` or `MOVING_AVG(value, 10)`.
447    Function(FunctionCall),
448    /// A prediction expression like `PREDICT(value, HOLT_WINTERS(24), horizon=24)`.
449    Predict(PredictExpr),
450    /// An aliased projection (`<inner> AS <alias>`).
451    Aliased {
452        /// Inner projection.
453        inner: Box<Projection>,
454        /// Alias name.
455        alias: String,
456    },
457}
458
459/// A generic function call used as a projection (or inside HAVING).
460#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
461pub struct FunctionCall {
462    /// Function name (e.g., `"MOVING_AVG"`, `"RATE"`).
463    pub name: String,
464    /// Arguments as AST expressions (stringified for now).
465    pub args: Vec<String>,
466}
467
468/// `PREDICT(...)` expression.
469#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
470pub struct PredictExpr {
471    /// Method name (e.g., `"HOLT_WINTERS"`, `"EXP_SMOOTHING"`).
472    pub method: String,
473    /// Forecast horizon in number of points.
474    pub horizon: u64,
475    /// Optional sampling interval (e.g., `"1h"`).
476    pub interval: Option<String>,
477    /// Additional keyword arguments.
478    pub args: Vec<(String, String)>,
479}
480
481// ═══════════════════════════════════════════════════════════════════════
482//  Display implementations — pretty-print AST
483// ═══════════════════════════════════════════════════════════════════════
484
485impl fmt::Display for QueryAst {
486    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
487        match self {
488            QueryAst::Select(q) => write!(f, "{}", q),
489            QueryAst::Find(q) => write!(f, "FIND {:?}", q.filter.raw),
490            QueryAst::Aggregate(q) => write!(f, "{}", q),
491            QueryAst::TemporalJoin(q) => {
492                write!(f, "{} TEMPORAL JOIN {} {}", q.left, q.right, q.join_type)
493            }
494            QueryAst::Stream(q) => write!(f, "CREATE STREAM {} AS {}", q.name, q.body),
495            QueryAst::Explain(inner) => write!(f, "EXPLAIN {}", inner),
496            QueryAst::Macro(m) => {
497                write!(
498                    f,
499                    "CREATE MACRO {}({}) AS {}",
500                    m.name,
501                    m.params.join(", "),
502                    m.body
503                )
504            }
505            QueryAst::Lineage(l) => write!(f, "LINEAGE({})", l.key),
506        }
507    }
508}
509
510impl fmt::Display for SelectQuery {
511    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
512        write!(f, "SELECT ")?;
513        if self.projections.is_empty() {
514            write!(f, "*")?;
515        } else {
516            for (i, p) in self.projections.iter().enumerate() {
517                if i > 0 {
518                    write!(f, ", ")?;
519                }
520                write!(f, "{}", p)?;
521            }
522        }
523        if let Some(from) = &self.from {
524            write!(f, " FROM {}", from)?;
525        }
526        if let Some(w) = &self.where_clause {
527            write!(f, " WHERE {}", w.root)?;
528        }
529        if let Some(order) = &self.order_by {
530            write!(f, " ORDER BY {:?} {:?}", order.field, order.direction)?;
531        }
532        if let Some(lim) = self.limit {
533            write!(f, " LIMIT {}", lim)?;
534        }
535        if let Some(off) = self.offset {
536            write!(f, " OFFSET {}", off)?;
537        }
538        Ok(())
539    }
540}
541
542impl fmt::Display for AggregateQuery {
543    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
544        write!(f, "SELECT ")?;
545        for (i, a) in self.aggregations.iter().enumerate() {
546            if i > 0 {
547                write!(f, ", ")?;
548            }
549            write!(f, "{:?}", a)?;
550        }
551        if let Some(from) = &self.from {
552            write!(f, " FROM {}", from)?;
553        }
554        if let Some(w) = &self.where_clause {
555            write!(f, " WHERE {}", w.root)?;
556        }
557        if let Some(g) = &self.group_by {
558            write!(f, " GROUP BY {:?}", g)?;
559        }
560        Ok(())
561    }
562}
563
564impl fmt::Display for KeyPattern {
565    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
566        match self {
567            KeyPattern::Exact(s) => write!(f, "\"{}\"", s),
568            KeyPattern::Prefix(s) => write!(f, "\"{}*\"", s),
569            KeyPattern::Glob(s) => write!(f, "\"{}\"", s),
570            KeyPattern::Regex(s) => write!(f, "REGEX(\"{}\")", s),
571            KeyPattern::Union(parts) => {
572                for (i, p) in parts.iter().enumerate() {
573                    if i > 0 {
574                        write!(f, " OR ")?;
575                    }
576                    write!(f, "{}", p)?;
577                }
578                Ok(())
579            }
580        }
581    }
582}
583
584impl fmt::Display for Condition {
585    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
586        match self {
587            Condition::Comparison { field, op, rhs } => {
588                write!(f, "{:?} {:?} {:?}", field, op, rhs)
589            }
590            Condition::Key(kp) => write!(f, "key MATCHES {}", kp),
591            Condition::TimeRange(tr) => {
592                write!(f, "timestamp BETWEEN {:?} AND {:?}", tr.start, tr.end)
593            }
594            Condition::Anomaly(m) => write!(f, "ANOMALY(value, {:?})", m),
595            Condition::Pattern(p) => write!(f, "MATCHES_PATTERN(value, {:?})", p),
596            Condition::Freshness(fc) => write!(f, "{:?}", fc),
597            Condition::Similarity(s) => write!(f, "SIMILAR_TO(<vec>, {})", s.k),
598            Condition::Boolean(BooleanOp::And(cs)) => {
599                for (i, c) in cs.iter().enumerate() {
600                    if i > 0 {
601                        write!(f, " AND ")?;
602                    }
603                    write!(f, "({})", c)?;
604                }
605                Ok(())
606            }
607            Condition::Boolean(BooleanOp::Or(cs)) => {
608                for (i, c) in cs.iter().enumerate() {
609                    if i > 0 {
610                        write!(f, " OR ")?;
611                    }
612                    write!(f, "({})", c)?;
613                }
614                Ok(())
615            }
616            Condition::Boolean(BooleanOp::Not(c)) => write!(f, "NOT ({})", c),
617        }
618    }
619}
620
621impl fmt::Display for Projection {
622    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623        match self {
624            Projection::All => write!(f, "*"),
625            Projection::Key => write!(f, "key"),
626            Projection::Value => write!(f, "value"),
627            Projection::Timestamp => write!(f, "timestamp"),
628            Projection::Function(fc) => write!(f, "{}({})", fc.name, fc.args.join(", ")),
629            Projection::Predict(p) => write!(f, "PREDICT({}, horizon={})", p.method, p.horizon),
630            Projection::Aliased { inner, alias } => write!(f, "{} AS {}", inner, alias),
631        }
632    }
633}
634
635// ═══════════════════════════════════════════════════════════════════════
636//  Unit tests
637// ═══════════════════════════════════════════════════════════════════════
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642
643    #[test]
644    fn serde_roundtrip_simple_select() {
645        let ast = QueryAst::Select(SelectQuery {
646            projections: vec![Projection::All],
647            from: Some(KeyPattern::Glob("sensor/*".into())),
648            where_clause: None,
649            order_by: Some(OrderBy {
650                field: OrderField::Timestamp,
651                direction: Direction::Asc,
652            }),
653            limit: Some(100),
654            offset: None,
655        });
656
657        let bytes = bincode::serialize(&ast).unwrap();
658        let decoded: QueryAst = bincode::deserialize(&bytes).unwrap();
659        assert_eq!(ast, decoded);
660    }
661
662    #[test]
663    fn display_pretty_prints_select() {
664        let ast = QueryAst::Select(SelectQuery {
665            projections: vec![Projection::Key, Projection::Value],
666            from: Some(KeyPattern::Prefix("sensor/".into())),
667            where_clause: None,
668            order_by: None,
669            limit: Some(10),
670            offset: None,
671        });
672        let s = format!("{}", ast);
673        assert!(s.contains("SELECT"));
674        assert!(s.contains("FROM"));
675        assert!(s.contains("LIMIT 10"));
676    }
677}