Skip to main content

fraiseql_cli/schema/intermediate/
analytics.rs

1//! Fact table/aggregate structs: `IntermediateFactTable`, `IntermediateMeasure`,
2//! `IntermediateDimensions`, `IntermediateDimensionPath`, `IntermediateFilter`,
3//! `IntermediateAggregateQuery`.
4
5use std::collections::HashMap;
6
7use fraiseql_core::compiler::fact_table::FactRowScope;
8use serde::{Deserialize, Serialize};
9
10// =============================================================================
11// Analytics Definitions
12// =============================================================================
13
14/// Fact table definition in intermediate format (Analytics)
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct IntermediateFactTable {
17    /// Name of the fact table
18    pub table_name:               String,
19    /// The type the fact table is read as: its measures, filters and dimension paths are
20    /// fields of that type, gated as a read of it (accepts `type` too).
21    #[serde(default, alias = "type", skip_serializing_if = "Option::is_none")]
22    pub type_name:                Option<String>,
23    /// `"relation"` when the relation filters its own rows by tenant, on values the server
24    /// sets per request (#1799). Absent: the server scopes them, or refuses the read.
25    #[serde(default, skip_serializing_if = "FactRowScope::is_server")]
26    pub row_scope:                FactRowScope,
27    /// The row filters the server applies to the table's aggregate and window roots (#1800),
28    /// one per denormalized filter column: `{"tenant_id": "jwt:org_id"}` or the nested form
29    /// every SDK emits. Absent: the linked type's own `inject_params` scope them, when it
30    /// declares any.
31    #[serde(
32        default,
33        with = "super::operations::inject_params_serde",
34        skip_serializing_if = "indexmap::IndexMap::is_empty"
35    )]
36    pub inject_params:            indexmap::IndexMap<String, String>,
37    /// Measure columns (numeric aggregates)
38    pub measures:                 Vec<IntermediateMeasure>,
39    /// Dimension metadata
40    pub dimensions:               IntermediateDimensions,
41    /// Denormalized filter columns
42    pub denormalized_filters:     Vec<IntermediateFilter>,
43    /// Maps JSONB measure paths to flat SQL column names for pre-aggregated views
44    #[serde(default)]
45    pub native_measures:          HashMap<String, String>,
46    /// Maps deep JSONB dimension paths to flat SQL column names for GROUP BY
47    #[serde(default)]
48    pub native_dimension_mapping: HashMap<String, String>,
49}
50
51/// Measure column definition
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct IntermediateMeasure {
54    /// Measure column name
55    pub name:       String,
56    /// SQL data type of the measure
57    pub sql_type:   String,
58    /// Whether the column can be NULL
59    pub nullable:   bool,
60    /// How the measure aggregates over time (#1459). Omitted: additive.
61    #[serde(
62        default,
63        skip_serializing_if = "fraiseql_core::compiler::fact_table::Additivity::is_additive"
64    )]
65    pub additivity: fraiseql_core::compiler::fact_table::Additivity,
66}
67
68/// Dimensions metadata
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct IntermediateDimensions {
71    /// Dimension name
72    pub name:  String,
73    /// Paths to dimension fields within JSONB
74    pub paths: Vec<IntermediateDimensionPath>,
75}
76
77/// Dimension path within JSONB
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct IntermediateDimensionPath {
80    /// Path name identifier
81    pub name:      String,
82    /// JSON path (accepts both "`json_path`" and "path" for cross-language compat)
83    #[serde(alias = "path")]
84    pub json_path: String,
85    /// Data type (accepts both "`data_type`" and "type" for cross-language compat)
86    #[serde(alias = "type")]
87    pub data_type: String,
88}
89
90/// Denormalized filter column
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct IntermediateFilter {
93    /// Filter column name
94    pub name:      String,
95    /// SQL data type of the filter
96    pub sql_type:  String,
97    /// Whether this column should be indexed
98    pub indexed:   bool,
99    /// For an `LTREE` path column: the `[hierarchies.<name>]` its paths belong to, so
100    /// `descendant_of_id` / `ancestor_of_id` can resolve a node id (#1498).
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub hierarchy: Option<String>,
103}
104
105/// Aggregate query definition (Analytics)
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct IntermediateAggregateQuery {
108    /// Aggregate query name
109    pub name:            String,
110    /// Fact table to aggregate from
111    pub fact_table:      String,
112    /// Automatically generate GROUP BY clauses
113    pub auto_group_by:   bool,
114    /// Automatically generate aggregate functions
115    pub auto_aggregates: bool,
116    /// Optional description
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub description:     Option<String>,
119}