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_definitions(&self, name: &str)
18        -> Result<Vec<crate::ast::ColumnDef>, SQLError>;
19}
20
21#[derive(Clone, Default)]
22pub(super) struct RuleBindingContext<'a> {
23    pub(super) catalog: Option<&'a dyn RuleSourceCatalog>,
24    pub(super) relations_bound: bool,
25    pub(super) ctes: BTreeMap<String, Vec<String>>,
26}
27
28impl<'a> RuleBindingContext<'a> {
29    pub(super) fn with_catalog(catalog: &'a dyn RuleSourceCatalog, relations_bound: bool) -> Self {
30        Self {
31            catalog: Some(catalog),
32            relations_bound,
33            ctes: BTreeMap::new(),
34        }
35    }
36
37    pub(super) fn relation_columns(&self, name: &str) -> Result<Vec<String>, SQLError> {
38        if let Some(columns) = self.ctes.get(&name.to_ascii_lowercase()) {
39            return Ok(columns.clone());
40        }
41        self.catalog.map_or_else(
42            || Ok(Vec::new()),
43            |catalog| {
44                catalog
45                    .query_source_columns(name, self.relations_bound)?
46                    .ok_or_else(|| SQLError::UnknownTable(name.to_string()))
47            },
48        )
49    }
50
51    pub(super) fn with_ctes(&self, ctes: &[CTE]) -> Result<Self, SQLError> {
52        let mut context = self.clone();
53        for cte in ctes {
54            let key = cte.name.to_ascii_lowercase();
55            if cte.recursive {
56                context.ctes.entry(key.clone()).or_default();
57            }
58            let mut columns = cte_output_columns(&cte.body, &context)?;
59            apply_positional_aliases(&mut columns, &cte.columns);
60            if let Some(search) = &cte.search {
61                columns.push(search.sequence_column.clone());
62            }
63            if let Some(cycle) = &cte.cycle {
64                columns.push(cycle.mark_column.clone());
65                columns.push(cycle.path_column.clone());
66            }
67            context.ctes.insert(key, columns);
68        }
69        Ok(context)
70    }
71}
72
73pub fn rule_action_target_row_type(
74    catalog: &dyn RuleSourceCatalog,
75    action: &Statement,
76) -> Result<Vec<(String, ColumnType)>, SQLError> {
77    let table = match action {
78        Statement::Insert(statement) => &statement.table,
79        Statement::Update(statement) => &statement.table,
80        Statement::Delete(statement) => &statement.table,
81        _ => return Ok(Vec::new()),
82    };
83    Ok(catalog
84        .rule_relation_definitions(table)?
85        .into_iter()
86        .map(|column| (column.name, column.ty))
87        .collect())
88}
89pub fn rule_action_target_columns(
90    catalog: &dyn RuleSourceCatalog,
91    action: &Statement,
92) -> Result<std::collections::BTreeSet<String>, SQLError> {
93    Ok(rule_action_target_row_type(catalog, action)?
94        .into_iter()
95        .map(|(column, _)| column)
96        .collect())
97}