Skip to main content

activecube_rs/compiler/
ir.rs

1use std::sync::Arc;
2
3/// SQL binding value — database-agnostic representation.
4#[derive(Debug, Clone)]
5pub enum SqlValue {
6    String(String),
7    Int(i64),
8    Float(f64),
9    Bool(bool),
10    /// Raw SQL expression (not parameterized). Used for `now() - INTERVAL ...` etc.
11    Expression(String),
12}
13
14/// JOIN type for cross-cube relationships.
15#[derive(Debug, Clone, PartialEq, Eq, Default)]
16pub enum JoinType {
17    #[default]
18    Left,
19    Inner,
20    Full,
21    Cross,
22}
23
24impl JoinType {
25    pub fn sql_keyword(&self) -> &'static str {
26        match self {
27            JoinType::Left => "LEFT JOIN",
28            JoinType::Inner => "INNER JOIN",
29            JoinType::Full => "FULL OUTER JOIN",
30            JoinType::Cross => "CROSS JOIN",
31        }
32    }
33}
34
35/// Custom query builder that bypasses the standard SQL compilation pipeline.
36/// Implementors produce SQL directly from a `QueryIR` for cubes that need
37/// window functions, CTEs, or multi-step subqueries.
38#[derive(Clone)]
39pub struct QueryBuilderFn(pub Arc<dyn Fn(&QueryIR) -> CompileResult + Send + Sync>);
40
41impl std::fmt::Debug for QueryBuilderFn {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.write_str("QueryBuilderFn(...)")
44    }
45}
46
47/// Intermediate representation of a compiled GraphQL cube query.
48#[derive(Debug, Clone)]
49pub struct QueryIR {
50    pub cube: String,
51    pub schema: String,
52    pub table: String,
53    pub selects: Vec<SelectExpr>,
54    pub filters: FilterNode,
55    pub having: FilterNode,
56    pub group_by: Vec<String>,
57    pub order_by: Vec<OrderExpr>,
58    pub limit: u32,
59    pub offset: u32,
60    /// ClickHouse `LIMIT n BY col1, col2` — per-group row limit without aggregation.
61    pub limit_by: Option<LimitByExpr>,
62    /// When true, append FINAL after FROM for ReplacingMergeTree tables.
63    pub use_final: bool,
64    /// JOIN expressions to other cubes, resolved at query time.
65    pub joins: Vec<JoinExpr>,
66    /// Custom query builder that overrides standard SQL compilation.
67    pub custom_query_builder: Option<QueryBuilderFn>,
68    /// Expanded subquery SQL for FROM clause. When present, the compiler
69    /// generates `FROM ({subquery}) AS _t` instead of `FROM schema.table`.
70    pub from_subquery: Option<String>,
71}
72
73/// A resolved JOIN to another table, appended to the outer query.
74#[derive(Debug, Clone)]
75pub struct JoinExpr {
76    pub schema: String,
77    pub table: String,
78    /// SQL alias for this join, e.g. "_j0", "_j1"
79    pub alias: String,
80    /// (main_table_col, joined_table_col) ON conditions
81    pub conditions: Vec<(String, String)>,
82    /// Fields requested from the joined table
83    pub selects: Vec<SelectExpr>,
84    /// Non-aggregate columns for GROUP BY (mode B only)
85    pub group_by: Vec<String>,
86    /// Append FINAL for ReplacingMergeTree targets (mode A)
87    pub use_final: bool,
88    /// true = target is AggregatingMergeTree, use subquery JOIN (mode B)
89    pub is_aggregate: bool,
90    /// Target cube name for result mapping
91    pub target_cube: String,
92    /// GraphQL field name for result nesting, e.g. "joinBuyToken"
93    pub join_field: String,
94    /// JOIN type — defaults to Left for backward compatibility.
95    pub join_type: JoinType,
96}
97
98/// Bitquery-style dimension aggregation type.
99/// `PostBalance(maximum: Block_Slot)` → `argMax(post_balance, block_slot)`
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum DimAggType {
102    ArgMax,
103    ArgMin,
104}
105
106#[derive(Debug, Clone)]
107pub enum SelectExpr {
108    Column {
109        column: String,
110        alias: Option<String>,
111    },
112    Aggregate {
113        function: String,
114        column: String,
115        alias: String,
116        condition: Option<String>,
117    },
118    /// Dimension-level aggregation: `argMax(value_column, compare_column)`.
119    /// Used for Bitquery patterns like `PostBalance(maximum: Block_Slot)`.
120    DimAggregate {
121        agg_type: DimAggType,
122        value_column: String,
123        compare_column: String,
124        alias: String,
125        condition: Option<String>,
126    },
127}
128
129#[derive(Debug, Clone)]
130pub enum FilterNode {
131    And(Vec<FilterNode>),
132    Or(Vec<FilterNode>),
133    Condition {
134        column: String,
135        op: CompareOp,
136        value: SqlValue,
137    },
138    /// Array-level includes filter: "exists an element in the parallel arrays
139    /// satisfying all conditions". Compiles to `arrayExists(lambda, arrays)`.
140    ArrayIncludes {
141        /// ClickHouse column names of the parallel arrays participating in the lambda.
142        array_columns: Vec<String>,
143        /// Each inner Vec is one `includes` object (conditions AND-ed within).
144        /// Multiple inner Vecs are AND-ed as separate `arrayExists` calls.
145        element_conditions: Vec<Vec<FilterNode>>,
146    },
147    Empty,
148}
149
150#[derive(Debug, Clone)]
151pub enum CompareOp {
152    Eq,
153    Ne,
154    Gt,
155    Ge,
156    Lt,
157    Le,
158    Like,
159    In,
160    NotIn,
161    Includes,
162    IsNull,
163    IsNotNull,
164}
165
166impl CompareOp {
167    pub fn sql_op(&self) -> &'static str {
168        match self {
169            CompareOp::Eq => "=",
170            CompareOp::Ne => "!=",
171            CompareOp::Gt => ">",
172            CompareOp::Ge => ">=",
173            CompareOp::Lt => "<",
174            CompareOp::Le => "<=",
175            CompareOp::Like => "LIKE",
176            CompareOp::In => "IN",
177            CompareOp::NotIn => "NOT IN",
178            CompareOp::Includes => "LIKE",
179            CompareOp::IsNull => "IS NULL",
180            CompareOp::IsNotNull => "IS NOT NULL",
181        }
182    }
183
184    pub fn is_unary(&self) -> bool {
185        matches!(self, CompareOp::IsNull | CompareOp::IsNotNull)
186    }
187}
188
189#[derive(Debug, Clone)]
190pub struct OrderExpr {
191    pub column: String,
192    pub descending: bool,
193}
194
195#[derive(Debug, Clone)]
196pub struct LimitByExpr {
197    pub count: u32,
198    pub offset: u32,
199    pub columns: Vec<String>,
200}
201
202impl FilterNode {
203    pub fn is_empty(&self) -> bool {
204        matches!(self, FilterNode::Empty)
205    }
206}
207
208/// Result of SQL compilation, including alias remapping for HAVING support.
209pub struct CompileResult {
210    pub sql: String,
211    pub bindings: Vec<SqlValue>,
212    /// Alias → original column name. Used to remap ClickHouse JSON keys
213    /// back to the column names that resolvers expect.
214    pub alias_remap: Vec<(String, String)>,
215}