use crate::{
compiler::{
aggregation::{AggregateSelection, AggregationRequest, GroupBySelection},
fact_table::FactTableMetadata,
window_functions::{
PartitionByColumn, WindowFunctionSpec, WindowRequest, WindowSelectColumn,
},
},
db::WhereClause,
error::{FraiseQLError, Result},
schema::{CompiledSchema, TypeDefinition, fact_field},
security::SecurityContext,
};
fn linked_type<'a>(
schema: &'a CompiledSchema,
metadata: &FactTableMetadata,
) -> Option<&'a TypeDefinition> {
schema
.get_fact_table(&metadata.table_name)
.and_then(|registered| schema.fact_table_type(registered))
.or_else(|| schema.fact_table_type(metadata))
}
struct References<'r> {
names: Vec<(&'r str, &'static str)>,
}
impl<'r> References<'r> {
const fn new() -> Self {
Self { names: Vec::new() }
}
fn add(&mut self, name: &'r str, usage: &'static str) {
self.names.push((name, usage));
}
fn add_where(&mut self, clause: &'r WhereClause) -> Result<()> {
match clause {
WhereClause::And(clauses) | WhereClause::Or(clauses) => {
clauses.iter().try_for_each(|c| self.add_where(c))
},
WhereClause::Not(inner) | WhereClause::Typed { inner, .. } => self.add_where(inner),
WhereClause::Field { path, .. } => {
path.first().map_or(Ok(()), |key| {
self.add(key, "filter by");
Ok(())
})
},
WhereClause::NativeField { column, .. } => {
self.add(column, "filter by");
Ok(())
},
_ => Err(FraiseQLError::Validation {
message: "unsupported condition in an aggregate or window filter".to_string(),
path: None,
}),
}
}
fn add_aggregate(&mut self, aggregate: &'r AggregateSelection) {
match aggregate {
AggregateSelection::CountDistinct { field, .. }
| AggregateSelection::BoolAggregate { field, .. } => self.add(field, "aggregate"),
AggregateSelection::MeasureAggregate { measure, .. } => self.add(measure, "aggregate"),
AggregateSelection::Count { .. } => {},
}
}
}
fn group_by_name<'m>(selection: &'m GroupBySelection, metadata: &'m FactTableMetadata) -> &'m str {
match selection {
GroupBySelection::Dimension { path, .. } => path,
GroupBySelection::TemporalBucket { column, .. } => column,
GroupBySelection::CalendarDimension { source_column, .. } => source_column,
GroupBySelection::NativeDimension { column, .. } => metadata
.native_dimension_mapping
.iter()
.find(|(_, mapped)| *mapped == column)
.map_or(column.as_str(), |(key, _)| key.as_str()),
}
}
pub(super) fn refuse_unreadable_aggregate(
schema: &CompiledSchema,
metadata: &FactTableMetadata,
request: &AggregationRequest,
security_context: Option<&SecurityContext>,
) -> Result<()> {
let Some(type_def) = linked_type(schema, metadata) else {
return Ok(());
};
let mut refs = References::new();
if let Some(clause) = &request.where_clause {
refs.add_where(clause)?;
}
for selection in &request.group_by {
refs.add(group_by_name(selection, metadata), "group by");
}
for aggregate in &request.aggregates {
refs.add_aggregate(aggregate);
}
let outputs: Vec<&str> = request
.group_by
.iter()
.map(GroupBySelection::alias)
.chain(request.aggregates.iter().map(AggregateSelection::alias))
.collect();
for clause in &request.order_by {
if !outputs.contains(&clause.field.as_str()) {
refs.add(&clause.field, "order by");
}
}
refuse(schema, metadata, type_def, &refs, security_context)
}
pub(super) fn refuse_unreadable_window(
schema: &CompiledSchema,
metadata: &FactTableMetadata,
request: &WindowRequest,
security_context: Option<&SecurityContext>,
) -> Result<()> {
let Some(type_def) = linked_type(schema, metadata) else {
return Ok(());
};
let mut refs = References::new();
if let Some(clause) = &request.where_clause {
refs.add_where(clause)?;
}
for column in &request.select {
match column {
WindowSelectColumn::Measure { name, .. } | WindowSelectColumn::Filter { name, .. } => {
refs.add(name, "select");
},
WindowSelectColumn::Dimension { path, .. } => refs.add(path, "select"),
}
}
let outputs: Vec<&str> = request
.select
.iter()
.map(WindowSelectColumn::alias)
.chain(request.windows.iter().map(|w| w.alias.as_str()))
.collect();
for window in &request.windows {
match &window.function {
WindowFunctionSpec::Lag { field, .. }
| WindowFunctionSpec::Lead { field, .. }
| WindowFunctionSpec::FirstValue { field }
| WindowFunctionSpec::LastValue { field }
| WindowFunctionSpec::NthValue { field, .. }
| WindowFunctionSpec::RunningCountField { field } => refs.add(field, "window over"),
WindowFunctionSpec::RunningSum { measure }
| WindowFunctionSpec::RunningAvg { measure }
| WindowFunctionSpec::RunningMin { measure }
| WindowFunctionSpec::RunningMax { measure }
| WindowFunctionSpec::RunningStddev { measure }
| WindowFunctionSpec::RunningVariance { measure } => refs.add(measure, "window over"),
WindowFunctionSpec::RowNumber
| WindowFunctionSpec::Rank
| WindowFunctionSpec::DenseRank
| WindowFunctionSpec::Ntile { .. }
| WindowFunctionSpec::PercentRank
| WindowFunctionSpec::CumeDist
| WindowFunctionSpec::RunningCount => {},
}
for column in &window.partition_by {
match column {
PartitionByColumn::Dimension { path } => refs.add(path, "partition by"),
PartitionByColumn::Filter { name } | PartitionByColumn::Measure { name } => {
refs.add(name, "partition by");
},
}
}
for order in &window.order_by {
if !outputs.contains(&order.field.as_str()) {
refs.add(&order.field, "order by");
}
}
}
for order in &request.order_by {
if !outputs.contains(&order.field.as_str()) {
refs.add(&order.field, "order by");
}
}
refuse(schema, metadata, type_def, &refs, security_context)
}
fn refuse(
schema: &CompiledSchema,
metadata: &FactTableMetadata,
type_def: &TypeDefinition,
refs: &References<'_>,
security_context: Option<&SecurityContext>,
) -> Result<()> {
let type_name = type_def.name.as_str();
if let Some(role) = type_def.requires_role.as_deref() {
if !security_context.is_some_and(|ctx| ctx.roles.iter().any(|r| r == role)) {
return Err(FraiseQLError::Authorization {
message: format!(
"Access denied: fact table '{}' is read as '{type_name}', whose read \
requires a role the request does not hold",
metadata.table_name
),
action: Some("read".to_string()),
resource: Some(type_name.to_string()),
});
}
}
for &(name, usage) in &refs.names {
let Some(field) = fact_field(type_def, name) else {
return Err(FraiseQLError::Validation {
message: format!(
"'{name}' is not a field of '{type_name}', which fact table '{}' is read as",
metadata.table_name
),
path: None,
});
};
super::super::support::security::refuse_unreadable_reference(
schema,
type_name,
field,
security_context,
usage,
)?;
}
Ok(())
}