Skip to main content

activecube_rs/cube/
definition.rs

1use crate::compiler::ir::{JoinType, QueryBuilderFn};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub enum DimType {
5    String,
6    Int,
7    Float,
8    DateTime,
9    Bool,
10}
11
12#[derive(Debug, Clone)]
13pub struct Dimension {
14    pub graphql_name: String,
15    pub column: String,
16    pub dim_type: DimType,
17    pub description: Option<String>,
18}
19
20#[derive(Debug, Clone)]
21pub enum DimensionNode {
22    Leaf(Dimension),
23    Group {
24        graphql_name: String,
25        description: Option<String>,
26        children: Vec<DimensionNode>,
27    },
28}
29
30/// A named selector defines a filterable field on a Cube.
31/// Each selector maps a GraphQL argument name to a column + type,
32/// enabling `eq`, `gt`, `in`, `any` etc.
33#[derive(Debug, Clone)]
34pub struct SelectorDef {
35    pub graphql_name: String,
36    pub column: String,
37    pub dim_type: DimType,
38}
39
40pub fn selector(graphql_name: &str, column: &str, dim_type: DimType) -> SelectorDef {
41    SelectorDef {
42        graphql_name: graphql_name.to_string(),
43        column: column.to_string(),
44        dim_type,
45    }
46}
47
48/// Metric definition — standard SQL aggregate or custom expression.
49#[derive(Debug, Clone)]
50pub struct MetricDef {
51    pub name: String,
52    /// If None, uses the standard SQL function (COUNT/SUM/AVG/...).
53    /// If Some, uses this SQL template with `{column}` as placeholder.
54    /// Example: `"sumIf({column}, direction='in') - sumIf({column}, direction='out')"`
55    pub expression_template: Option<String>,
56    pub return_type: DimType,
57    pub description: Option<String>,
58    /// Whether this metric supports conditional aggregation (countIf/sumIf).
59    pub supports_where: bool,
60}
61
62impl MetricDef {
63    pub fn standard(name: &str) -> Self {
64        Self {
65            name: name.to_string(),
66            expression_template: None,
67            return_type: DimType::Float,
68            description: None,
69            supports_where: true,
70        }
71    }
72
73    pub fn custom(name: &str, expression: &str) -> Self {
74        Self {
75            name: name.to_string(),
76            expression_template: Some(expression.to_string()),
77            return_type: DimType::Float,
78            description: None,
79            supports_where: false,
80        }
81    }
82
83    pub fn with_description(mut self, desc: &str) -> Self {
84        self.description = Some(desc.to_string());
85        self
86    }
87
88    pub fn with_return_type(mut self, rt: DimType) -> Self {
89        self.return_type = rt;
90        self
91    }
92}
93
94/// Helper to create a list of standard metrics from names.
95pub fn standard_metrics(names: &[&str]) -> Vec<MetricDef> {
96    names.iter().map(|n| MetricDef::standard(n)).collect()
97}
98
99/// Multi-table routing: a single Cube can map to different physical tables
100/// depending on which columns the query requests.
101#[derive(Debug, Clone)]
102pub struct TableRoute {
103    pub schema: String,
104    pub table_pattern: String,
105    /// Columns available in this table. If empty, this route serves all queries.
106    pub available_columns: Vec<String>,
107    /// Lower priority = preferred. The primary table (schema/table_pattern) has implicit priority 0.
108    pub priority: u32,
109}
110
111/// Declares a JOIN relationship from this cube to another cube.
112#[derive(Debug, Clone)]
113pub struct JoinDef {
114    /// GraphQL field name on the source record, e.g. "joinTransfers"
115    pub field_name: String,
116    /// Target cube name as registered in the CubeRegistry, e.g. "Transfers"
117    pub target_cube: String,
118    /// (local_column, remote_column) pairs for the ON clause.
119    pub conditions: Vec<(String, String)>,
120    pub description: Option<String>,
121    /// JOIN type — defaults to Left.
122    pub join_type: JoinType,
123}
124
125pub fn join_def(field_name: &str, target_cube: &str, conditions: &[(&str, &str)]) -> JoinDef {
126    JoinDef {
127        field_name: field_name.to_string(),
128        target_cube: target_cube.to_string(),
129        conditions: conditions.iter().map(|(l, r)| (l.to_string(), r.to_string())).collect(),
130        description: None,
131        join_type: JoinType::Left,
132    }
133}
134
135pub fn join_def_desc(field_name: &str, target_cube: &str, conditions: &[(&str, &str)], desc: &str) -> JoinDef {
136    JoinDef {
137        field_name: field_name.to_string(),
138        target_cube: target_cube.to_string(),
139        conditions: conditions.iter().map(|(l, r)| (l.to_string(), r.to_string())).collect(),
140        description: Some(desc.to_string()),
141        join_type: JoinType::Left,
142    }
143}
144
145pub fn join_def_typed(
146    field_name: &str, target_cube: &str,
147    conditions: &[(&str, &str)],
148    join_type: JoinType,
149) -> JoinDef {
150    JoinDef {
151        field_name: field_name.to_string(),
152        target_cube: target_cube.to_string(),
153        conditions: conditions.iter().map(|(l, r)| (l.to_string(), r.to_string())).collect(),
154        description: None,
155        join_type,
156    }
157}
158
159#[derive(Debug, Clone)]
160pub struct CubeDefinition {
161    pub name: String,
162    pub schema: String,
163    /// Table name pattern. Use `{chain}` as placeholder for chain-prefixed tables
164    /// (e.g. `{chain}_trades` → `sol_trades`). For tables without chain prefix
165    /// (e.g. `dex_pool_liquidities`), use the literal table name and set
166    /// `chain_column` instead.
167    pub table_pattern: String,
168    /// If set, the table doesn't use a `{chain}` prefix in its name. Instead,
169    /// the chain is filtered via `WHERE <chain_column> = ?`. Example:
170    /// `dex_pool_liquidities` has a `chain` column rather than `sol_dex_pool_liquidities`.
171    pub chain_column: Option<String>,
172    pub dimensions: Vec<DimensionNode>,
173    pub metrics: Vec<MetricDef>,
174    pub selectors: Vec<SelectorDef>,
175    pub default_filters: Vec<(String, String)>,
176    pub default_limit: u32,
177    pub max_limit: u32,
178    /// Append FINAL to FROM clause for ReplacingMergeTree tables in ClickHouse.
179    pub use_final: bool,
180    /// Human-readable description of the cube's purpose, exposed via _cubeMetadata.
181    pub description: String,
182    /// Declarative JOIN relationships to other cubes.
183    pub joins: Vec<JoinDef>,
184    /// Alternative tables that can serve subsets of this cube's columns.
185    /// When non-empty, `resolve_table` picks the best match by requested columns.
186    pub table_routes: Vec<TableRoute>,
187    /// Custom query builder that bypasses the standard IR → SQL compilation.
188    /// Used for cubes requiring window functions, CTEs, or multi-step subqueries.
189    pub custom_query_builder: Option<QueryBuilderFn>,
190    /// SQL subquery used as the FROM source instead of `schema.table`.
191    /// Supports `{schema}` and `{chain}` placeholders expanded at query time.
192    /// When set, the compiler generates `FROM ({expanded}) AS _t`.
193    pub from_subquery: Option<String>,
194}
195
196impl CubeDefinition {
197    pub fn table_for_chain(&self, chain: &str) -> String {
198        self.table_pattern.replace("{chain}", chain)
199    }
200
201    pub fn qualified_table(&self, chain: &str) -> String {
202        format!("{}.{}", self.schema, self.table_for_chain(chain))
203    }
204
205    /// Pick the optimal (schema, table) for a given chain and set of requested columns.
206    /// Falls back to the primary schema/table_pattern when no route matches.
207    pub fn resolve_table(&self, chain: &str, requested_columns: &[String]) -> (String, String) {
208        if self.table_routes.is_empty() {
209            return (self.schema.clone(), self.table_for_chain(chain));
210        }
211
212        let mut candidates: Vec<&TableRoute> = self.table_routes.iter()
213            .filter(|r| {
214                r.available_columns.is_empty()
215                    || requested_columns.iter().all(|c| r.available_columns.contains(c))
216            })
217            .collect();
218
219        candidates.sort_by_key(|r| r.priority);
220
221        if let Some(best) = candidates.first() {
222            (best.schema.clone(), best.table_pattern.replace("{chain}", chain))
223        } else {
224            (self.schema.clone(), self.table_for_chain(chain))
225        }
226    }
227
228    pub fn flat_dimensions(&self) -> Vec<(String, Dimension)> {
229        let mut out = Vec::new();
230        for node in &self.dimensions {
231            collect_leaves(node, "", &mut out);
232        }
233        out
234    }
235
236    /// Check if a metric name exists in this cube's metrics.
237    pub fn has_metric(&self, name: &str) -> bool {
238        self.metrics.iter().any(|m| m.name == name)
239    }
240
241    /// Find a metric definition by name.
242    pub fn find_metric(&self, name: &str) -> Option<&MetricDef> {
243        self.metrics.iter().find(|m| m.name == name)
244    }
245}
246
247fn collect_leaves(node: &DimensionNode, prefix: &str, out: &mut Vec<(String, Dimension)>) {
248    match node {
249        DimensionNode::Leaf(dim) => {
250            let path = if prefix.is_empty() {
251                dim.graphql_name.clone()
252            } else {
253                format!("{}_{}", prefix, dim.graphql_name)
254            };
255            out.push((path, dim.clone()));
256        }
257        DimensionNode::Group { graphql_name, children, .. } => {
258            let new_prefix = if prefix.is_empty() {
259                graphql_name.clone()
260            } else {
261                format!("{prefix}_{graphql_name}")
262            };
263            for child in children {
264                collect_leaves(child, &new_prefix, out);
265            }
266        }
267    }
268}
269
270// ---------------------------------------------------------------------------
271// CubeBuilder — ergonomic builder pattern for CubeDefinition
272// ---------------------------------------------------------------------------
273
274pub struct CubeBuilder {
275    def: CubeDefinition,
276}
277
278impl CubeBuilder {
279    pub fn new(name: &str) -> Self {
280        Self {
281            def: CubeDefinition {
282                name: name.to_string(),
283                schema: String::new(),
284                table_pattern: String::new(),
285                chain_column: None,
286                dimensions: Vec::new(),
287                metrics: Vec::new(),
288                selectors: Vec::new(),
289                default_filters: Vec::new(),
290                default_limit: 25,
291                max_limit: 10000,
292                use_final: false,
293                description: String::new(),
294                joins: Vec::new(),
295                table_routes: Vec::new(),
296                custom_query_builder: None,
297                from_subquery: None,
298            },
299        }
300    }
301
302    pub fn schema(mut self, schema: &str) -> Self {
303        self.def.schema = schema.to_string();
304        self
305    }
306
307    pub fn table(mut self, pattern: &str) -> Self {
308        self.def.table_pattern = pattern.to_string();
309        self
310    }
311
312    pub fn chain_column(mut self, column: &str) -> Self {
313        self.def.chain_column = Some(column.to_string());
314        self
315    }
316
317    pub fn dimension(mut self, node: DimensionNode) -> Self {
318        self.def.dimensions.push(node);
319        self
320    }
321
322    /// Add a standard metric (count, sum, avg, min, max, uniq).
323    pub fn metric(mut self, name: &str) -> Self {
324        self.def.metrics.push(MetricDef::standard(name));
325        self
326    }
327
328    /// Add multiple standard metrics by name.
329    pub fn metrics(mut self, names: &[&str]) -> Self {
330        self.def.metrics.extend(names.iter().map(|s| MetricDef::standard(s)));
331        self
332    }
333
334    /// Add a custom metric with an SQL expression template.
335    pub fn custom_metric(mut self, def: MetricDef) -> Self {
336        self.def.metrics.push(def);
337        self
338    }
339
340    pub fn selector(mut self, sel: SelectorDef) -> Self {
341        self.def.selectors.push(sel);
342        self
343    }
344
345    pub fn default_filter(mut self, column: &str, value: &str) -> Self {
346        self.def.default_filters.push((column.to_string(), value.to_string()));
347        self
348    }
349
350    pub fn default_limit(mut self, limit: u32) -> Self {
351        self.def.default_limit = limit;
352        self
353    }
354
355    pub fn max_limit(mut self, limit: u32) -> Self {
356        self.def.max_limit = limit;
357        self
358    }
359
360    pub fn use_final(mut self, val: bool) -> Self {
361        self.def.use_final = val;
362        self
363    }
364
365    pub fn description(mut self, desc: &str) -> Self {
366        self.def.description = desc.to_string();
367        self
368    }
369
370    pub fn join(mut self, j: JoinDef) -> Self {
371        self.def.joins.push(j);
372        self
373    }
374
375    pub fn joins(mut self, js: Vec<JoinDef>) -> Self {
376        self.def.joins.extend(js);
377        self
378    }
379
380    pub fn table_route(mut self, route: TableRoute) -> Self {
381        self.def.table_routes.push(route);
382        self
383    }
384
385    pub fn custom_query_builder(mut self, builder: QueryBuilderFn) -> Self {
386        self.def.custom_query_builder = Some(builder);
387        self
388    }
389
390    pub fn from_subquery(mut self, subquery_sql: &str) -> Self {
391        self.def.from_subquery = Some(subquery_sql.to_string());
392        self
393    }
394
395    pub fn build(self) -> CubeDefinition {
396        self.def
397    }
398}
399
400// ---------------------------------------------------------------------------
401// Helper functions for concise dimension/selector construction
402// ---------------------------------------------------------------------------
403
404pub fn dim(graphql_name: &str, column: &str, dim_type: DimType) -> DimensionNode {
405    DimensionNode::Leaf(Dimension {
406        graphql_name: graphql_name.to_string(),
407        column: column.to_string(),
408        dim_type,
409        description: None,
410    })
411}
412
413pub fn dim_desc(graphql_name: &str, column: &str, dim_type: DimType, desc: &str) -> DimensionNode {
414    DimensionNode::Leaf(Dimension {
415        graphql_name: graphql_name.to_string(),
416        column: column.to_string(),
417        dim_type,
418        description: Some(desc.to_string()),
419    })
420}
421
422pub fn dim_group(graphql_name: &str, children: Vec<DimensionNode>) -> DimensionNode {
423    DimensionNode::Group {
424        graphql_name: graphql_name.to_string(),
425        description: None,
426        children,
427    }
428}
429
430pub fn dim_group_desc(graphql_name: &str, desc: &str, children: Vec<DimensionNode>) -> DimensionNode {
431    DimensionNode::Group {
432        graphql_name: graphql_name.to_string(),
433        description: Some(desc.to_string()),
434        children,
435    }
436}