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}
67
68/// A resolved JOIN to another table, appended to the outer query.
69#[derive(Debug, Clone)]
70pub struct JoinExpr {
71    pub schema: String,
72    pub table: String,
73    /// SQL alias for this join, e.g. "_j0", "_j1"
74    pub alias: String,
75    /// (main_table_col, joined_table_col) ON conditions
76    pub conditions: Vec<(String, String)>,
77    /// Fields requested from the joined table
78    pub selects: Vec<SelectExpr>,
79    /// Non-aggregate columns for GROUP BY (mode B only)
80    pub group_by: Vec<String>,
81    /// Append FINAL for ReplacingMergeTree targets (mode A)
82    pub use_final: bool,
83    /// true = target is AggregatingMergeTree, use subquery JOIN (mode B)
84    pub is_aggregate: bool,
85    /// Target cube name for result mapping
86    pub target_cube: String,
87    /// GraphQL field name for result nesting, e.g. "joinBuyToken"
88    pub join_field: String,
89    /// JOIN type — defaults to Left for backward compatibility.
90    pub join_type: JoinType,
91}
92
93#[derive(Debug, Clone)]
94pub enum SelectExpr {
95    Column {
96        column: String,
97        alias: Option<String>,
98    },
99    Aggregate {
100        function: String,
101        column: String,
102        alias: String,
103        condition: Option<String>,
104    },
105}
106
107#[derive(Debug, Clone)]
108pub enum FilterNode {
109    And(Vec<FilterNode>),
110    Or(Vec<FilterNode>),
111    Condition {
112        column: String,
113        op: CompareOp,
114        value: SqlValue,
115    },
116    Empty,
117}
118
119#[derive(Debug, Clone)]
120pub enum CompareOp {
121    Eq,
122    Ne,
123    Gt,
124    Ge,
125    Lt,
126    Le,
127    Like,
128    In,
129    NotIn,
130    Includes,
131    IsNull,
132    IsNotNull,
133}
134
135impl CompareOp {
136    pub fn sql_op(&self) -> &'static str {
137        match self {
138            CompareOp::Eq => "=",
139            CompareOp::Ne => "!=",
140            CompareOp::Gt => ">",
141            CompareOp::Ge => ">=",
142            CompareOp::Lt => "<",
143            CompareOp::Le => "<=",
144            CompareOp::Like => "LIKE",
145            CompareOp::In => "IN",
146            CompareOp::NotIn => "NOT IN",
147            CompareOp::Includes => "LIKE",
148            CompareOp::IsNull => "IS NULL",
149            CompareOp::IsNotNull => "IS NOT NULL",
150        }
151    }
152
153    pub fn is_unary(&self) -> bool {
154        matches!(self, CompareOp::IsNull | CompareOp::IsNotNull)
155    }
156}
157
158#[derive(Debug, Clone)]
159pub struct OrderExpr {
160    pub column: String,
161    pub descending: bool,
162}
163
164#[derive(Debug, Clone)]
165pub struct LimitByExpr {
166    pub count: u32,
167    pub offset: u32,
168    pub columns: Vec<String>,
169}
170
171impl FilterNode {
172    pub fn is_empty(&self) -> bool {
173        matches!(self, FilterNode::Empty)
174    }
175}
176
177/// Result of SQL compilation, including alias remapping for HAVING support.
178pub struct CompileResult {
179    pub sql: String,
180    pub bindings: Vec<SqlValue>,
181    /// Alias → original column name. Used to remap ClickHouse JSON keys
182    /// back to the column names that resolvers expect.
183    pub alias_remap: Vec<(String, String)>,
184}