Skip to main content

fraiseql_core/schema/compiled/
fact_table_links.rs

1//! The fact-table → type link: refusal of a link the runtime cannot enforce (ruling AB 1).
2//!
3//! A fact table linked to a type (`FactTableMetadata::type_name`) is read as that type: an
4//! aggregate or a window over it references its measures, filters and dimension paths as
5//! fields of the type, and each reference is gated as a read of that field. A declared column
6//! the type does not have would be a column no gate can reach, so the link must be complete —
7//! and the type must exist. Refused at load, whatever produced the schema, naming the fact
8//! table, the type and the names.
9
10use super::CompiledSchema;
11use crate::{
12    compiler::fact_table::FactTableMetadata,
13    schema::{FieldDefinition, TypeDefinition},
14};
15
16/// The field of `type_def` a fact-table name refers to: matched on its `snake_case` form, as a
17/// query filter's storage key is (`support::security`), so `organizationId` and
18/// `organization_id` are one field.
19pub fn fact_field<'a>(type_def: &'a TypeDefinition, name: &str) -> Option<&'a FieldDefinition> {
20    let key = crate::utils::to_snake_case(name);
21    type_def
22        .fields
23        .iter()
24        .find(|f| crate::utils::to_snake_case(f.name.as_str()) == key)
25}
26
27/// Every name a fact table declares: what a request over it can reference.
28fn declared_names(metadata: &FactTableMetadata) -> Vec<&str> {
29    let mut names: Vec<&str> = metadata
30        .measures
31        .iter()
32        .map(|m| m.name.as_str())
33        .chain(metadata.denormalized_filters.iter().map(|f| f.name.as_str()))
34        .chain(metadata.dimensions.paths.iter().map(|p| p.name.as_str()))
35        .chain(metadata.calendar_dimensions.iter().map(|c| c.source_column.as_str()))
36        .chain(metadata.native_measures.keys().map(String::as_str))
37        .chain(metadata.native_dimension_mapping.keys().map(String::as_str))
38        .collect();
39    names.sort_unstable();
40    names.dedup();
41    names
42}
43
44impl CompiledSchema {
45    /// The type a fact table is read as, when it is linked to one that exists.
46    #[must_use]
47    pub fn fact_table_type(&self, metadata: &FactTableMetadata) -> Option<&TypeDefinition> {
48        let name = metadata.type_name.as_deref()?;
49        self.types.iter().find(|t| t.name == name)
50    }
51
52    /// Fact tables whose link to a type cannot be enforced: the type does not exist, or it
53    /// lacks a name the fact table declares. One message per fact table, empty when every
54    /// link is enforceable; unlinked fact tables are not concerned.
55    #[must_use]
56    pub fn fact_table_link_violations(&self) -> Vec<String> {
57        let mut tables: Vec<&FactTableMetadata> = self.fact_tables.values().collect();
58        tables.sort_by(|a, b| a.table_name.cmp(&b.table_name));
59        tables
60            .into_iter()
61            .filter_map(|metadata| {
62                let type_name = metadata.type_name.as_deref()?;
63                let Some(type_def) = self.fact_table_type(metadata) else {
64                    return Some(format!(
65                        "fact table '{}' is read as type '{type_name}', which the schema does \
66                         not declare",
67                        metadata.table_name
68                    ));
69                };
70                let missing: Vec<&str> = declared_names(metadata)
71                    .into_iter()
72                    .filter(|name| fact_field(type_def, name).is_none())
73                    .collect();
74                (!missing.is_empty()).then(|| {
75                    format!(
76                        "fact table '{}' is read as type '{type_name}', which has no field for \
77                         {}: a column the type does not declare is one no read gate can reach. \
78                         Declare the fields on '{type_name}', or drop them from the fact table.",
79                        metadata.table_name,
80                        missing.iter().map(|n| format!("'{n}'")).collect::<Vec<_>>().join(", ")
81                    )
82                })
83            })
84            .collect()
85    }
86}