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