Skip to main content

uqa_sql/semantics/rules/action_binding/
context.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Read-only relation shapes used to bind stored rule actions.
8use super::{apply_positional_aliases, cte_output_columns, BTreeMap, SQLError, CTE};
9use crate::ast::{ColumnType, Statement};
10
11pub trait RuleSourceCatalog {
12    fn query_source_columns(
13        &self,
14        name: &str,
15        relations_bound: bool,
16    ) -> Result<Option<Vec<String>>, SQLError>;
17    fn rule_relation_columns(&self, name: &str) -> Result<Vec<(String, ColumnType)>, SQLError>;
18}
19
20#[derive(Clone, Default)]
21pub(super) struct RuleBindingContext<'a> {
22    pub(super) catalog: Option<&'a dyn RuleSourceCatalog>,
23    pub(super) relations_bound: bool,
24    pub(super) ctes: BTreeMap<String, Vec<String>>,
25}
26
27impl<'a> RuleBindingContext<'a> {
28    pub(super) fn with_catalog(catalog: &'a dyn RuleSourceCatalog, relations_bound: bool) -> Self {
29        Self {
30            catalog: Some(catalog),
31            relations_bound,
32            ctes: BTreeMap::new(),
33        }
34    }
35
36    pub(super) fn relation_columns(&self, name: &str) -> Result<Vec<String>, SQLError> {
37        if let Some(columns) = self.ctes.get(&name.to_ascii_lowercase()) {
38            return Ok(columns.clone());
39        }
40        self.catalog.map_or_else(
41            || Ok(Vec::new()),
42            |catalog| {
43                catalog
44                    .query_source_columns(name, self.relations_bound)?
45                    .ok_or_else(|| SQLError::UnknownTable(name.to_string()))
46            },
47        )
48    }
49
50    pub(super) fn with_ctes(&self, ctes: &[CTE]) -> Result<Self, SQLError> {
51        let mut context = self.clone();
52        for cte in ctes {
53            let key = cte.name.to_ascii_lowercase();
54            if cte.recursive {
55                context.ctes.entry(key.clone()).or_default();
56            }
57            let mut columns = cte_output_columns(&cte.body, &context)?;
58            apply_positional_aliases(&mut columns, &cte.columns);
59            if let Some(search) = &cte.search {
60                columns.push(search.sequence_column.clone());
61            }
62            if let Some(cycle) = &cte.cycle {
63                columns.push(cycle.mark_column.clone());
64                columns.push(cycle.path_column.clone());
65            }
66            context.ctes.insert(key, columns);
67        }
68        Ok(context)
69    }
70}
71
72pub fn rule_action_target_row_type(
73    catalog: &dyn RuleSourceCatalog,
74    action: &Statement,
75) -> Result<Vec<(String, ColumnType)>, SQLError> {
76    let table = match action {
77        Statement::Insert(statement) => &statement.table,
78        Statement::Update(statement) => &statement.table,
79        Statement::Delete(statement) => &statement.table,
80        _ => return Ok(Vec::new()),
81    };
82    catalog.rule_relation_columns(table)
83}
84pub fn rule_action_target_columns(
85    catalog: &dyn RuleSourceCatalog,
86    action: &Statement,
87) -> Result<std::collections::BTreeSet<String>, SQLError> {
88    Ok(rule_action_target_row_type(catalog, action)?
89        .into_iter()
90        .map(|(column, _)| column)
91        .collect())
92}