uqa-sql 0.5.2

PostgreSQL-compatible SQL compiler built on libpg_query
Documentation
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

//! Bound rule and trigger dependencies observed at each catalog read boundary.
use super::lookup::EventLookupContext;
use crate::{
    catalog::events::{PreparedRuleColumnDrop, RuleColumnDependency, StoredRule},
    SQLError,
};
use std::collections::BTreeMap;
use uqa_core::RelationIdentity;
pub struct EventColumnDependencies {
    pub triggers: Vec<String>,
    pub rules: Vec<(RelationIdentity, String)>,
}
impl EventLookupContext<'_> {
    pub fn prepare_rule_column_drop(
        &self,
        table: &str,
        column: &str,
    ) -> Result<PreparedRuleColumnDrop, String> {
        let relation = RelationIdentity::from_legacy_name(table)?;
        let dependency = RuleColumnDependency {
            relation: relation.clone(),
            column: column.to_string(),
        };
        let mut rules = self.registry.read_rules().clone();
        let mut rebind = std::collections::BTreeSet::new();
        for (event_relation, entries) in &mut rules {
            for (name, rule) in entries {
                let dependencies = rule.dependencies.as_ref().ok_or_else(|| {
                    format!(
                        "rule `{}` on `{}` has no bound dependency state",
                        rule.definition.name,
                        event_relation.qualified_name()
                    )
                })?;
                if dependencies.columns.contains(&dependency) {
                    return Err(format!(
                        "cannot drop column {column} of table {table} because rule {} on {} depends on it",
                        rule.definition.name,
                        event_relation.qualified_name()
                    ));
                }
                if event_relation != &relation && !dependencies.relations.contains(&relation) {
                    continue;
                }
                crate::binding::stored_columns::remove_rule_source_column_aliases(
                    self.analysis.columns,
                    &mut rule.definition,
                    &dependency,
                )
                .map_err(|error| {
                    format!(
                        "reshape rule `{}` source aliases before column drop: {error}",
                        rule.definition.name
                    )
                })?;
                rebind.insert((event_relation.clone(), name.clone()));
            }
        }
        Ok(PreparedRuleColumnDrop { rules, rebind })
    }

    pub fn column_event_dependencies(
        &self,
        table: &str,
        column: &str,
    ) -> Result<EventColumnDependencies, SQLError> {
        let relation = RelationIdentity::from_legacy_name(table).map_err(|error| {
            SQLError::Internal(format!("decode trigger relation `{table}`: {error}"))
        })?;
        let dependent_triggers = self
            .registry
            .read_triggers()
            .get(&relation)
            .into_iter()
            .flat_map(BTreeMap::values)
            .filter(|trigger| {
                trigger
                    .definition
                    .update_columns
                    .iter()
                    .any(|name| name == column)
                    || trigger.definition.when.as_ref().is_some_and(|condition| {
                        crate::schema::dependencies::schema_expr_references_column(
                            condition, column,
                        )
                    })
            })
            .map(|trigger| trigger.definition.name.clone())
            .collect::<Vec<_>>();
        let rules = self.registry.read_rules();
        let dependent_rules = dependent_rules_for_column(&rules, &relation, column)?;
        drop(rules);
        Ok(EventColumnDependencies {
            triggers: dependent_triggers,
            rules: dependent_rules,
        })
    }
}

fn dependent_rules_for_column(
    rules: &BTreeMap<RelationIdentity, BTreeMap<String, StoredRule>>,
    relation: &RelationIdentity,
    column: &str,
) -> Result<Vec<(RelationIdentity, String)>, SQLError> {
    let dependency = RuleColumnDependency {
        relation: relation.clone(),
        column: column.to_string(),
    };
    let mut dependent = Vec::new();
    for (event_relation, entries) in rules {
        for rule in entries.values() {
            let dependencies = rule.dependencies.as_ref().ok_or_else(|| {
                SQLError::Internal(format!(
                    "rule `{}` on `{}` has no bound dependency state",
                    rule.definition.name,
                    event_relation.qualified_name()
                ))
            })?;
            if dependencies.columns.contains(&dependency) {
                dependent.push((event_relation.clone(), rule.definition.name.clone()));
            }
        }
    }
    Ok(dependent)
}