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}