use std::fmt;
use indexmap::IndexMap;
use super::{CompiledSchema, type_inject::describe_source};
use crate::{
compiler::fact_table::{FactRowScope, FactTableMetadata},
schema::{InjectedParamSource, TypeDefinition},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FactInjectOrigin<'a> {
Declared,
Inherited(&'a str),
}
impl fmt::Display for FactInjectOrigin<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Declared => f.write_str("its own inject params"),
Self::Inherited(type_name) => {
write!(f, "the inject params inherited from type '{type_name}'")
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FactInjectScope<'a> {
pub params: &'a IndexMap<String, InjectedParamSource>,
pub origin: FactInjectOrigin<'a>,
}
impl CompiledSchema {
#[must_use]
pub fn unenforced_tenant_scope(&self, metadata: &FactTableMetadata) -> Option<String> {
let registered = self.get_fact_table(&metadata.table_name);
let deciding = registered.unwrap_or(metadata);
if deciding.row_scope == FactRowScope::Relation
|| self.fact_table_inject_scope(deciding).is_some()
{
return None;
}
let reason = registered
.and_then(|table| self.tenant_scope_reason(table))
.or_else(|| self.tenant_scope_reason(metadata))?;
let fault = self.fact_table_type(deciding).and_then(|type_def| {
self.inheritance_fault(deciding, type_def).map(|fault| {
format!(
"; {} cannot scope it: {fault}",
FactInjectOrigin::Inherited(type_def.name.as_str())
)
})
});
Some(reason + fault.as_deref().unwrap_or_default())
}
#[must_use]
pub fn fact_table_inject_scope<'a>(
&'a self,
table: &'a FactTableMetadata,
) -> Option<FactInjectScope<'a>> {
if table.row_scope == FactRowScope::Relation {
return None;
}
if !table.inject_params.is_empty() {
return Some(FactInjectScope {
params: &table.inject_params,
origin: FactInjectOrigin::Declared,
});
}
let type_def = self.fact_table_type(table)?;
if type_def.inject_params.is_empty() || self.inheritance_fault(table, type_def).is_some() {
return None;
}
Some(FactInjectScope {
params: &type_def.inject_params,
origin: FactInjectOrigin::Inherited(type_def.name.as_str()),
})
}
#[must_use]
pub fn fact_table_inject_violations(&self) -> Vec<String> {
let mut tables: Vec<&FactTableMetadata> = self.fact_tables.values().collect();
tables.sort_by(|a, b| a.table_name.cmp(&b.table_name));
let mut violations = Vec::new();
for table in tables {
let name = &table.table_name;
if table.inject_params.is_empty() {
continue;
}
if table.row_scope == FactRowScope::Relation {
violations.push(format!(
"fact table '{name}' declares row_scope = \"relation\" (its relation filters \
its own rows) and inject_params (the server filters them): declare one"
));
}
let linked = self.fact_table_type(table);
for (column, source) in &table.inject_params {
if let Some(fault) = column_fault(table, column, source) {
violations.push(format!("fact table '{name}': inject param {fault}"));
}
let Some(type_def) = linked else { continue };
if let Some(type_source) = type_def.inject_params.get(column) {
if type_source != source {
violations.push(format!(
"fact table '{name}' and type '{}' both scope '{column}', from \
different sources: the fact table from {}, the type from {}. Give \
them the same source, or drop the fact table's.",
type_def.name,
describe_source(source),
describe_source(type_source),
));
}
}
}
}
violations
}
fn inheritance_fault(
&self,
table: &FactTableMetadata,
type_def: &TypeDefinition,
) -> Option<String> {
type_def
.inject_params
.iter()
.find_map(|(column, source)| column_fault(table, column, source))
}
#[must_use]
pub fn fact_table_scope_violations(&self) -> Vec<String> {
let mut tables: Vec<&FactTableMetadata> = self.fact_tables.values().collect();
tables.sort_by(|a, b| a.table_name.cmp(&b.table_name));
tables
.into_iter()
.filter_map(|table| {
self.unenforced_tenant_scope(table)
.map(|reason| format!("fact table '{}' ({reason})", table.table_name))
})
.collect()
}
fn tenant_scope_reason(&self, table: &FactTableMetadata) -> Option<String> {
self.linked_type_reason(table).or_else(|| self.injected_column_reason(table))
}
fn linked_type_reason(&self, table: &FactTableMetadata) -> Option<String> {
let type_def = self.fact_table_type(table)?;
if let Some(column) = type_def.inject_params.keys().next() {
return Some(format!(
"it is read as type '{}', which injects '{column}'",
type_def.name
));
}
self.queries
.iter()
.filter(|q| q.return_type == type_def.name)
.find_map(|q| q.inject_params.keys().next().map(|column| (q, column)))
.map(|(query, column)| {
format!(
"it is read as type '{}', which query '{}' reads injecting '{column}'",
type_def.name, query.name
)
})
}
fn injected_column_reason(&self, table: &FactTableMetadata) -> Option<String> {
table.denormalized_filters.iter().find_map(|filter| {
let column = filter.name.as_str();
self.queries
.iter()
.find(|q| q.inject_params.contains_key(column))
.map(|q| format!("its column '{column}' is injected by query '{}'", q.name))
.or_else(|| {
self.types
.iter()
.find(|t| t.inject_params.contains_key(column))
.map(|t| format!("its column '{column}' is injected by type '{}'", t.name))
})
})
}
}
fn column_fault(
table: &FactTableMetadata,
column: &str,
source: &InjectedParamSource,
) -> Option<String> {
if !table.denormalized_filters.iter().any(|filter| filter.name == column) {
let mut filters: Vec<String> =
table.denormalized_filters.iter().map(|f| format!("'{}'", f.name)).collect();
filters.sort();
return Some(format!(
"'{column}' is not one of its denormalized filter columns ({})",
if filters.is_empty() {
"it has none".to_string()
} else {
filters.join(", ")
}
));
}
if let InjectedParamSource::JwtOptional(claim) = source {
return Some(format!(
"'{column}' is 'jwt_optional:{claim}', but it filters rows, and an absent claim \
cannot: use 'jwt:{claim}'"
));
}
None
}