qail_core/
ast.rs

1//! Abstract Syntax Tree for QAIL commands.
2//!
3//! This module defines the core data structures that represent
4//! a parsed QAIL query.
5
6use serde::{Deserialize, Serialize};
7
8/// The primary command structure representing a parsed QAIL query.
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub struct QailCmd {
11    /// The action to perform (GET, SET, DEL, ADD)
12    pub action: Action,
13    /// Target table name
14    pub table: String,
15    /// Columns to select/return
16    pub columns: Vec<Column>,
17    /// Joins to other tables
18    #[serde(default)]
19    pub joins: Vec<Join>,
20    /// Cages (filters, sorts, limits, payloads)
21    pub cages: Vec<Cage>,
22    /// Whether to use DISTINCT in SELECT
23    #[serde(default)]
24    pub distinct: bool,
25    /// Index definition (for Action::Index)
26    #[serde(default)]
27    pub index_def: Option<IndexDef>,
28    /// Table-level constraints (for Action::Make)
29    #[serde(default)]
30    pub table_constraints: Vec<TableConstraint>,
31    /// Set operations (UNION, INTERSECT, EXCEPT) chained queries
32    #[serde(default)]
33    pub set_ops: Vec<(SetOp, Box<QailCmd>)>,
34    /// HAVING clause conditions (filter on aggregates)
35    #[serde(default)]
36    pub having: Vec<Condition>,
37    /// GROUP BY mode (Simple, Rollup, Cube)
38    #[serde(default)]
39    pub group_by_mode: GroupByMode,
40    /// CTE definition (for WITH/WITH RECURSIVE queries)
41    #[serde(default)]
42    pub cte: Option<CTEDef>,
43}
44
45impl QailCmd {
46    /// Create a new GET command for the given table.
47    pub fn get(table: impl Into<String>) -> Self {
48        Self {
49            action: Action::Get,
50            table: table.into(),
51            joins: vec![],
52            columns: vec![],
53            cages: vec![],
54            distinct: false,
55            index_def: None,
56            table_constraints: vec![],
57            set_ops: vec![],
58            having: vec![],
59            group_by_mode: GroupByMode::Simple,
60            cte: None,
61        }
62    }
63
64    /// Create a new SET (update) command for the given table.
65    pub fn set(table: impl Into<String>) -> Self {
66        Self {
67            action: Action::Set,
68            table: table.into(),
69            joins: vec![],
70            columns: vec![],
71            cages: vec![],
72            distinct: false,
73            index_def: None,
74            table_constraints: vec![],
75            set_ops: vec![],
76            having: vec![],
77            group_by_mode: GroupByMode::Simple,
78            cte: None,
79        }
80    }
81
82    /// Create a new DEL (delete) command for the given table.
83    pub fn del(table: impl Into<String>) -> Self {
84        Self {
85            action: Action::Del,
86            table: table.into(),
87            joins: vec![],
88            columns: vec![],
89            cages: vec![],
90            distinct: false,
91            index_def: None,
92            table_constraints: vec![],
93            set_ops: vec![],
94            having: vec![],
95            group_by_mode: GroupByMode::Simple,
96            cte: None,
97        }
98    }
99
100    /// Create a new ADD (insert) command for the given table.
101    pub fn add(table: impl Into<String>) -> Self {
102        Self {
103            action: Action::Add,
104            table: table.into(),
105            joins: vec![],
106            columns: vec![],
107            cages: vec![],
108            distinct: false,
109            index_def: None,
110            table_constraints: vec![],
111            set_ops: vec![],
112            having: vec![],
113            group_by_mode: GroupByMode::Simple,
114            cte: None,
115        }
116    }
117    /// Add columns to hook (select).
118    pub fn hook(mut self, cols: &[&str]) -> Self {
119        self.columns = cols.iter().map(|c| Column::Named(c.to_string())).collect();
120        self
121    }
122
123    /// Add a filter cage.
124    pub fn cage(mut self, column: &str, value: impl Into<Value>) -> Self {
125        self.cages.push(Cage {
126            kind: CageKind::Filter,
127            conditions: vec![Condition {
128                column: column.to_string(),
129                op: Operator::Eq,
130                value: value.into(),
131                is_array_unnest: false,
132            }],
133            logical_op: LogicalOp::And,
134        });
135        self
136    }
137
138    /// Add a limit cage.
139    pub fn limit(mut self, n: i64) -> Self {
140        self.cages.push(Cage {
141            kind: CageKind::Limit(n as usize),
142            conditions: vec![],
143            logical_op: LogicalOp::And,
144        });
145        self
146    }
147
148    /// Add a sort cage (ascending).
149    pub fn sort_asc(mut self, column: &str) -> Self {
150        self.cages.push(Cage {
151            kind: CageKind::Sort(SortOrder::Asc),
152            conditions: vec![Condition {
153                column: column.to_string(),
154                op: Operator::Eq,
155                value: Value::Null,
156                is_array_unnest: false,
157            }],
158            logical_op: LogicalOp::And,
159        });
160        self
161    }
162
163    /// Add a sort cage (descending).
164    pub fn sort_desc(mut self, column: &str) -> Self {
165        self.cages.push(Cage {
166            kind: CageKind::Sort(SortOrder::Desc),
167            conditions: vec![Condition {
168                column: column.to_string(),
169                op: Operator::Eq,
170                value: Value::Null,
171                is_array_unnest: false,
172            }],
173            logical_op: LogicalOp::And,
174        });
175        self
176    }
177}
178
179/// A join definition.
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181pub struct Join {
182    pub table: String,
183    pub kind: JoinKind,
184}
185
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub enum JoinKind {
188    Inner,
189    Left,
190    Right,
191    /// LATERAL join (Postgres, MySQL 8+) - allows subquery to reference outer query
192    Lateral,
193}
194
195/// Set operation type for combining queries
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
197pub enum SetOp {
198    /// UNION (removes duplicates)
199    Union,
200    /// UNION ALL (keeps duplicates)
201    UnionAll,
202    /// INTERSECT (common rows)
203    Intersect,
204    /// EXCEPT (rows in first but not second)
205    Except,
206}
207
208/// GROUP BY mode for advanced aggregations
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
210pub enum GroupByMode {
211    /// Standard GROUP BY
212    #[default]
213    Simple,
214    /// ROLLUP - hierarchical subtotals
215    Rollup,
216    /// CUBE - all combinations of subtotals
217    Cube,
218}
219
220/// CTE (Common Table Expression) definition
221#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
222pub struct CTEDef {
223    /// CTE name (the alias used in the query)
224    pub name: String,
225    /// Whether this is a RECURSIVE CTE
226    pub recursive: bool,
227    /// Column list for the CTE (optional)
228    pub columns: Vec<String>,
229    /// Base query (non-recursive part)
230    pub base_query: Box<QailCmd>,
231    /// Recursive part (UNION ALL with self-reference)
232    pub recursive_query: Option<Box<QailCmd>>,
233    /// Source table for recursive join (references CTE name)
234    pub source_table: Option<String>,
235}
236
237/// A column reference.
238#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
239pub enum Column {
240    /// All columns (*)
241    Star,
242    /// A named column
243    Named(String),
244    /// An aliased column (col AS alias)
245    Aliased { name: String, alias: String },
246    /// An aggregate function (COUNT(col))
247    Aggregate { col: String, func: AggregateFunc },
248    /// Column Definition (for Make keys)
249    Def {
250        name: String,
251        data_type: String,
252        constraints: Vec<Constraint>,
253    },
254    /// Column Modification (for Mod keys)
255    Mod {
256        kind: ModKind,
257        col: Box<Column>,
258    },
259    /// Window Function Definition
260    Window {
261        name: String,
262        func: String,
263        params: Vec<Value>,
264        partition: Vec<String>,
265        order: Vec<Cage>,
266    },
267    /// CASE WHEN expression
268    Case {
269        /// WHEN condition THEN value pairs
270        when_clauses: Vec<(Condition, Value)>,
271        /// ELSE value (optional)
272        else_value: Option<Box<Value>>,
273        /// Optional alias
274        alias: Option<String>,
275    },
276}
277
278impl std::fmt::Display for Column {
279    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280        match self {
281            Column::Star => write!(f, "*"),
282            Column::Named(name) => write!(f, "{}", name),
283            Column::Aliased { name, alias } => write!(f, "{} AS {}", name, alias),
284            Column::Aggregate { col, func } => write!(f, "{}({})", func, col),
285            Column::Def {
286                name,
287                data_type,
288                constraints,
289            } => {
290                write!(f, "{}:{}", name, data_type)?;
291                for c in constraints {
292                    write!(f, "^{}", c)?;
293                }
294                Ok(())
295            }
296            Column::Mod { kind, col } => match kind {
297                ModKind::Add => write!(f, "+{}", col),
298                ModKind::Drop => write!(f, "-{}", col),
299            },
300            Column::Window { name, func, params, partition, order } => {
301                write!(f, "{}:{}(", name, func)?;
302                for (i, p) in params.iter().enumerate() {
303                    if i > 0 { write!(f, ", ")?; }
304                    write!(f, "{}", p)?;
305                }
306                write!(f, ")")?;
307                
308                // Print partitions if any (custom syntax for display?)
309                if !partition.is_empty() {
310                    write!(f, "{{Part=")?;
311                    for (i, p) in partition.iter().enumerate() {
312                        if i > 0 { write!(f, ",")?; }
313                        write!(f, "{}", p)?;
314                    }
315                    write!(f, "}}")?;
316                }
317
318                // Print order cages (TODO: implement proper order display)
319                for _cage in order {
320                    // Order cages are sort cages - display format TBD
321                }
322                Ok(())
323            }
324            Column::Case { when_clauses, else_value, alias } => {
325                write!(f, "CASE")?;
326                for (cond, val) in when_clauses {
327                    write!(f, " WHEN {} THEN {}", cond.column, val)?;
328                }
329                if let Some(e) = else_value {
330                    write!(f, " ELSE {}", e)?;
331                }
332                write!(f, " END")?;
333                if let Some(a) = alias {
334                    write!(f, " AS {}", a)?;
335                }
336                Ok(())
337            }
338        }
339    }
340}
341
342/// Column modification type
343#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
344pub enum ModKind {
345    Add,
346    Drop,
347}
348
349/// Column definition constraints
350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
351pub enum Constraint {
352    PrimaryKey,
353    Unique,
354    Nullable,
355    /// DEFAULT value (e.g., `= uuid()`, `= 0`, `= now()`)
356    Default(String),
357    /// CHECK constraint with allowed values (e.g., `^check("a","b")`)
358    Check(Vec<String>),
359    /// Column comment (COMMENT ON COLUMN)
360    Comment(String),
361    /// Generated column expression (GENERATED ALWAYS AS)
362    Generated(ColumnGeneration),
363}
364
365/// Generated column type (STORED or VIRTUAL)
366#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
367pub enum ColumnGeneration {
368    /// GENERATED ALWAYS AS (expr) STORED - computed and stored
369    Stored(String),
370    /// GENERATED ALWAYS AS (expr) - computed at query time (default in Postgres 18+)
371    Virtual(String),
372}
373
374/// Window frame definition for window functions
375#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
376pub enum WindowFrame {
377    /// ROWS BETWEEN start AND end
378    Rows { start: FrameBound, end: FrameBound },
379    /// RANGE BETWEEN start AND end
380    Range { start: FrameBound, end: FrameBound },
381}
382
383/// Window frame boundary
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
385pub enum FrameBound {
386    UnboundedPreceding,
387    Preceding(i32),
388    CurrentRow,
389    Following(i32),
390    UnboundedFollowing,
391}
392
393impl std::fmt::Display for Constraint {
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        match self {
396            Constraint::PrimaryKey => write!(f, "pk"),
397            Constraint::Unique => write!(f, "uniq"),
398            Constraint::Nullable => write!(f, "?"),
399            Constraint::Default(val) => write!(f, "={}", val),
400            Constraint::Check(vals) => write!(f, "check({})", vals.join(",")),
401            Constraint::Comment(text) => write!(f, "comment(\"{}\")", text),
402            Constraint::Generated(generation) => match generation {
403                ColumnGeneration::Stored(expr) => write!(f, "gen({})", expr),
404                ColumnGeneration::Virtual(expr) => write!(f, "vgen({})", expr),
405            },
406        }
407    }
408}
409
410/// Index definition for CREATE INDEX
411#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
412pub struct IndexDef {
413    /// Index name
414    pub name: String,
415    /// Target table
416    pub table: String,
417    /// Columns to index (ordered)
418    pub columns: Vec<String>,
419    /// Whether this is a UNIQUE index
420    pub unique: bool,
421}
422
423/// Table-level constraints for composite keys
424#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
425pub enum TableConstraint {
426    /// UNIQUE (col1, col2, ...)
427    Unique(Vec<String>),
428    /// PRIMARY KEY (col1, col2, ...)
429    PrimaryKey(Vec<String>),
430}
431
432/// Aggregate functions.
433#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
434pub enum AggregateFunc {
435    Count,
436    Sum,
437    Avg,
438    Min,
439    Max,
440}
441
442impl std::fmt::Display for AggregateFunc {
443    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444        match self {
445            AggregateFunc::Count => write!(f, "COUNT"),
446            AggregateFunc::Sum => write!(f, "SUM"),
447            AggregateFunc::Avg => write!(f, "AVG"),
448            AggregateFunc::Min => write!(f, "MIN"),
449            AggregateFunc::Max => write!(f, "MAX"),
450        }
451    }
452}
453
454
455
456/// The action type (SQL operation).
457#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
458pub enum Action {
459    /// SELECT query
460    Get,
461    /// UPDATE query  
462    Set,
463    /// DELETE query
464    Del,
465    /// INSERT query
466    Add,
467    /// Generate Rust struct from table schema
468    Gen,
469    /// Create Table (Make)
470    Make,
471    /// Drop Table (Drop)
472    Drop,
473    /// Modify Table (Mod)
474    Mod,
475    /// Window Function (Over)
476    Over,
477    /// CTE (With)
478    With,
479    /// Create Index
480    /// Create Index
481    Index,
482    // Transactions
483    TxnStart,
484    TxnCommit,
485    TxnRollback,
486    Put,
487    DropCol,
488    RenameCol,
489    // Additional clauses
490    /// JSON_TABLE - convert JSON to relational rows
491    JsonTable,
492}
493
494impl std::fmt::Display for Action {
495    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
496        match self {
497            Action::Get => write!(f, "GET"),
498            Action::Set => write!(f, "SET"),
499            Action::Del => write!(f, "DEL"),
500            Action::Add => write!(f, "ADD"),
501            Action::Gen => write!(f, "GEN"),
502            Action::Make => write!(f, "MAKE"),
503            Action::Drop => write!(f, "DROP"),
504            Action::Mod => write!(f, "MOD"),
505            Action::Over => write!(f, "OVER"),
506            Action::With => write!(f, "WITH"),
507            Action::Index => write!(f, "INDEX"),
508            Action::TxnStart => write!(f, "TXN_START"),
509            Action::TxnCommit => write!(f, "TXN_COMMIT"),
510            Action::TxnRollback => write!(f, "TXN_ROLLBACK"),
511            Action::Put => write!(f, "PUT"),
512            Action::DropCol => write!(f, "DROP_COL"),
513            Action::RenameCol => write!(f, "RENAME_COL"),
514            Action::JsonTable => write!(f, "JSON_TABLE"),
515        }
516    }
517}
518
519
520
521/// A cage (constraint block) in the query.
522#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
523pub struct Cage {
524    /// The type of cage
525    pub kind: CageKind,
526    /// Conditions within this cage
527    pub conditions: Vec<Condition>,
528    /// Logical operator between conditions (AND or OR)
529    pub logical_op: LogicalOp,
530}
531
532/// The type of cage.
533#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
534pub enum CageKind {
535    /// WHERE filter
536    Filter,
537    /// SET payload (for updates)
538    Payload,
539    /// ORDER BY
540    Sort(SortOrder),
541    /// LIMIT
542    Limit(usize),
543    /// OFFSET
544    Offset(usize),
545    /// TABLESAMPLE - percentage of rows
546    Sample(usize),
547    /// QUALIFY - filter on window function results
548    Qualify,
549    /// PARTITION BY - window function partitioning
550    Partition,
551}
552
553/// Sort order direction.
554#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
555pub enum SortOrder {
556    Asc,
557    Desc,
558}
559
560/// Logical operator between conditions.
561#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
562pub enum LogicalOp {
563    #[default]
564    And,
565    Or,
566}
567
568/// A single condition within a cage.
569#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
570pub struct Condition {
571    /// Column name
572    pub column: String,
573    /// Comparison operator
574    pub op: Operator,
575    /// Value to compare against
576    pub value: Value,
577    /// Whether this is an array unnest operation (column[*])
578    #[serde(default)]
579    pub is_array_unnest: bool,
580}
581
582/// Comparison operators.
583#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
584pub enum Operator {
585    /// Equal (=)
586    Eq,
587    /// Not equal (!=, <>)
588    Ne,
589    /// Greater than (>)
590    Gt,
591    /// Greater than or equal (>=)
592    Gte,
593    /// Less than (<)
594    Lt,
595    /// Less than or equal (<=)  
596    Lte,
597    /// Fuzzy match (~) -> ILIKE
598    Fuzzy,
599    /// IN array
600    In,
601    /// NOT IN array
602    NotIn,
603    /// IS NULL
604    IsNull,
605    /// IS NOT NULL
606    IsNotNull,
607    /// JSON/Array Contains (@>)
608    Contains,
609    /// JSON Key Exists (?)
610    KeyExists,
611    /// JSON_EXISTS - check if path exists (Postgres 17+)
612    JsonExists,
613    /// JSON_QUERY - extract JSON object/array at path (Postgres 17+)
614    JsonQuery,
615    /// JSON_VALUE - extract scalar value at path (Postgres 17+)
616    JsonValue,
617}
618
619/// A value in a condition.
620#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
621pub enum Value {
622    /// NULL value
623    Null,
624    /// Boolean
625    Bool(bool),
626    /// Integer
627    Int(i64),
628    /// Float
629    Float(f64),
630    /// String
631    String(String),
632    /// Parameter reference ($1, $2, etc.)
633    Param(usize),
634    /// SQL function call (e.g., now())
635    Function(String),
636    /// Array of values
637    Array(Vec<Value>),
638    /// Subquery for IN/EXISTS expressions (e.g., WHERE id IN (SELECT ...))
639    Subquery(Box<QailCmd>),
640}
641
642impl From<bool> for Value {
643    fn from(b: bool) -> Self {
644        Value::Bool(b)
645    }
646}
647
648impl From<i32> for Value {
649    fn from(n: i32) -> Self {
650        Value::Int(n as i64)
651    }
652}
653
654impl From<i64> for Value {
655    fn from(n: i64) -> Self {
656        Value::Int(n)
657    }
658}
659
660impl From<f64> for Value {
661    fn from(n: f64) -> Self {
662        Value::Float(n)
663    }
664}
665
666impl From<&str> for Value {
667    fn from(s: &str) -> Self {
668        Value::String(s.to_string())
669    }
670}
671
672impl From<String> for Value {
673    fn from(s: String) -> Self {
674        Value::String(s)
675    }
676}
677
678impl std::fmt::Display for Value {
679    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
680        match self {
681            Value::Null => write!(f, "NULL"),
682            Value::Bool(b) => write!(f, "{}", b),
683            Value::Int(n) => write!(f, "{}", n),
684            Value::Float(n) => write!(f, "{}", n),
685            Value::String(s) => write!(f, "'{}'", s.replace('\'', "''")),
686            Value::Param(n) => write!(f, "${}", n),
687            Value::Function(name) => write!(f, "{}()", name),
688            Value::Array(arr) => {
689                write!(f, "ARRAY[")?;
690                for (i, v) in arr.iter().enumerate() {
691                    if i > 0 {
692                        write!(f, ", ")?;
693                    }
694                    write!(f, "{}", v)?;
695                }
696                write!(f, "]")
697            }
698            Value::Subquery(cmd) => write!(f, "({})", cmd.table), // Placeholder display
699        }
700    }
701}
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706
707    #[test]
708    fn test_builder_pattern() {
709        let cmd = QailCmd::get("users")
710            .hook(&["id", "email"])
711            .cage("active", true)
712            .limit(10);
713
714        assert_eq!(cmd.action, Action::Get);
715        assert_eq!(cmd.table, "users");
716        assert_eq!(cmd.columns.len(), 2);
717        assert_eq!(cmd.cages.len(), 2);
718    }
719}