Skip to main content

activecube_rs/compiler/
ir.rs

1/// SQL binding value — database-agnostic representation.
2#[derive(Debug, Clone)]
3pub enum SqlValue {
4    String(String),
5    Int(i64),
6    Float(f64),
7    Bool(bool),
8}
9
10/// Intermediate representation of a compiled GraphQL cube query.
11#[derive(Debug, Clone)]
12pub struct QueryIR {
13    pub cube: String,
14    pub schema: String,
15    pub table: String,
16    pub selects: Vec<SelectExpr>,
17    pub filters: FilterNode,
18    pub having: FilterNode,
19    pub group_by: Vec<String>,
20    pub order_by: Vec<OrderExpr>,
21    pub limit: u32,
22    pub offset: u32,
23    /// ClickHouse `LIMIT n BY col1, col2` — per-group row limit without aggregation.
24    pub limit_by: Option<LimitByExpr>,
25    /// When true, append FINAL after FROM for ReplacingMergeTree tables.
26    pub use_final: bool,
27    /// LEFT JOIN expressions to other cubes, resolved at query time.
28    pub joins: Vec<JoinExpr>,
29}
30
31/// A resolved LEFT JOIN to another table, appended to the outer query.
32#[derive(Debug, Clone)]
33pub struct JoinExpr {
34    pub schema: String,
35    pub table: String,
36    /// SQL alias for this join, e.g. "_j0", "_j1"
37    pub alias: String,
38    /// (main_table_col, joined_table_col) ON conditions
39    pub conditions: Vec<(String, String)>,
40    /// Fields requested from the joined table
41    pub selects: Vec<SelectExpr>,
42    /// Non-aggregate columns for GROUP BY (mode B only)
43    pub group_by: Vec<String>,
44    /// Append FINAL for ReplacingMergeTree targets (mode A)
45    pub use_final: bool,
46    /// true = target is AggregatingMergeTree, use subquery JOIN (mode B)
47    pub is_aggregate: bool,
48    /// Target cube name for result mapping
49    pub target_cube: String,
50    /// GraphQL field name for result nesting, e.g. "joinBuyToken"
51    pub join_field: String,
52}
53
54#[derive(Debug, Clone)]
55pub enum SelectExpr {
56    Column {
57        column: String,
58        alias: Option<String>,
59    },
60    Aggregate {
61        function: String,
62        column: String,
63        alias: String,
64        condition: Option<String>,
65    },
66}
67
68#[derive(Debug, Clone)]
69pub enum FilterNode {
70    And(Vec<FilterNode>),
71    Or(Vec<FilterNode>),
72    Condition {
73        column: String,
74        op: CompareOp,
75        value: SqlValue,
76    },
77    Empty,
78}
79
80#[derive(Debug, Clone)]
81pub enum CompareOp {
82    Eq,
83    Ne,
84    Gt,
85    Ge,
86    Lt,
87    Le,
88    Like,
89    In,
90    NotIn,
91    Includes,
92    IsNull,
93    IsNotNull,
94}
95
96impl CompareOp {
97    pub fn sql_op(&self) -> &'static str {
98        match self {
99            CompareOp::Eq => "=",
100            CompareOp::Ne => "!=",
101            CompareOp::Gt => ">",
102            CompareOp::Ge => ">=",
103            CompareOp::Lt => "<",
104            CompareOp::Le => "<=",
105            CompareOp::Like => "LIKE",
106            CompareOp::In => "IN",
107            CompareOp::NotIn => "NOT IN",
108            CompareOp::Includes => "LIKE",
109            CompareOp::IsNull => "IS NULL",
110            CompareOp::IsNotNull => "IS NOT NULL",
111        }
112    }
113
114    pub fn is_unary(&self) -> bool {
115        matches!(self, CompareOp::IsNull | CompareOp::IsNotNull)
116    }
117}
118
119#[derive(Debug, Clone)]
120pub struct OrderExpr {
121    pub column: String,
122    pub descending: bool,
123}
124
125#[derive(Debug, Clone)]
126pub struct LimitByExpr {
127    pub count: u32,
128    pub offset: u32,
129    pub columns: Vec<String>,
130}
131
132impl FilterNode {
133    pub fn is_empty(&self) -> bool {
134        matches!(self, FilterNode::Empty)
135    }
136}
137
138/// Result of SQL compilation, including alias remapping for HAVING support.
139pub struct CompileResult {
140    pub sql: String,
141    pub bindings: Vec<SqlValue>,
142    /// Alias → original column name. Used to remap ClickHouse JSON keys
143    /// back to the column names that resolvers expect.
144    pub alias_remap: Vec<(String, String)>,
145}