Skip to main content

uqa_sql/binding/
stored_relations.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Bind durable statement relations while preserving namespace, catalog, and error ordering.
8
9mod query;
10pub use query::{
11    bind_stored_query_relations, resolve_loaded_query_sequence, StoredQueryBindingContext,
12    StoredQueryNamespace, StoredQuerySequences,
13};
14
15use crate::{
16    ast::{Expr, Statement},
17    catalog::{
18        events::RuleDependencies,
19        resolution::{RelationLookupMode, RelationResolution},
20        stored_ast::StoredAstVisitor,
21    },
22    SQLError,
23};
24use std::collections::BTreeSet;
25use uqa_core::RelationIdentity;
26
27pub trait StoredRelationCatalog {
28    fn resolve_age_label_relation_name(&self, reference: &str) -> Result<Option<String>, SQLError>;
29    fn resolve_visible_relation_kind(
30        &self,
31        reference: &str,
32    ) -> Result<RelationResolution, SQLError>;
33    fn resolve_loaded_visible_relation_kind(
34        &self,
35        reference: &str,
36    ) -> Result<RelationResolution, SQLError>;
37    fn resolve_bound_relation_kind(&self, reference: &str) -> Result<RelationResolution, SQLError>;
38}
39
40fn bind_catalog_relation_reference(
41    catalog: &dyn StoredRelationCatalog,
42    reference: &mut String,
43    lookup_mode: RelationLookupMode,
44    loaded_catalog: bool,
45    context: &str,
46    dependencies: &mut BTreeSet<RelationIdentity>,
47) -> Result<(), SQLError> {
48    if lookup_mode == RelationLookupMode::Dynamic {
49        if let Some(canonical) = catalog.resolve_age_label_relation_name(reference)? {
50            let relation = RelationIdentity::from_legacy_name(&canonical).map_err(|error| {
51                SQLError::Internal(format!("decode bound rule source `{canonical}`: {error}"))
52            })?;
53            *reference = canonical;
54            dependencies.insert(relation);
55            return Ok(());
56        }
57    }
58    let resolution = match (lookup_mode, loaded_catalog) {
59        (RelationLookupMode::Dynamic, true) => {
60            catalog.resolve_loaded_visible_relation_kind(reference)?
61        }
62        (RelationLookupMode::Dynamic, false) => catalog.resolve_visible_relation_kind(reference)?,
63        (RelationLookupMode::Bound, _) => catalog.resolve_bound_relation_kind(reference)?,
64    };
65    let canonical = match resolution {
66        RelationResolution::Found(
67            canonical,
68            "table" | "view" | "materialized view" | "foreign table" | "sequence",
69        ) => canonical,
70        RelationResolution::Found(canonical, kind) => {
71            return Err(SQLError::Routine {
72                sqlstate: "42809".into(),
73                message: format!(
74                    "{context} source \"{canonical}\" is a {kind}, not a row relation"
75                ),
76            });
77        }
78        RelationResolution::MissingSchema(schema) => {
79            return Err(SQLError::Routine {
80                sqlstate: "3F000".into(),
81                message: format!("schema \"{schema}\" does not exist"),
82            });
83        }
84        RelationResolution::MissingRelation => {
85            return Err(SQLError::UnknownTable(reference.clone()));
86        }
87    };
88    let relation = RelationIdentity::from_legacy_name(&canonical).map_err(|error| {
89        SQLError::Internal(format!("decode bound rule source `{canonical}`: {error}"))
90    })?;
91    *reference = canonical;
92    if crate::catalog::VirtualRelation::at(&relation.schema, &relation.name).is_none() {
93        dependencies.insert(relation);
94    }
95    Ok(())
96}
97
98pub fn bind_rule_action_relation_dependencies(
99    catalog: &dyn StoredRelationCatalog,
100    statement: &mut Statement,
101    lookup_mode: RelationLookupMode,
102) -> Result<RuleDependencies, SQLError> {
103    let mut dependencies = BTreeSet::new();
104    let mut bind = |reference: &mut String| {
105        bind_catalog_relation_reference(
106            catalog,
107            reference,
108            lookup_mode,
109            false,
110            "CREATE RULE",
111            &mut dependencies,
112        )
113    };
114    let mut ignore_routine = |_: &mut String,
115                              _: Option<&mut Option<crate::ast::FunctionBinding>>|
116     -> Result<(), SQLError> { Ok(()) };
117    StoredAstVisitor {
118        source: None,
119        merge: None,
120        expression: None,
121        ty: None,
122        relation: &mut bind,
123        routine: &mut ignore_routine,
124    }
125    .bind_statement(statement)?;
126    Ok(RuleDependencies {
127        relations: dependencies,
128        columns: BTreeSet::new(),
129        routines: BTreeSet::new(),
130    })
131}
132
133pub fn bind_rule_condition_relation_dependencies(
134    catalog: &dyn StoredRelationCatalog,
135    expression: &mut Expr,
136    lookup_mode: RelationLookupMode,
137) -> Result<RuleDependencies, SQLError> {
138    let mut dependencies = BTreeSet::new();
139    let mut bind = |reference: &mut String| {
140        bind_catalog_relation_reference(
141            catalog,
142            reference,
143            lookup_mode,
144            false,
145            "CREATE RULE",
146            &mut dependencies,
147        )
148    };
149    let mut ignore_routine = |_: &mut String,
150                              _: Option<&mut Option<crate::ast::FunctionBinding>>|
151     -> Result<(), SQLError> { Ok(()) };
152    StoredAstVisitor {
153        source: None,
154        merge: None,
155        expression: None,
156        ty: None,
157        relation: &mut bind,
158        routine: &mut ignore_routine,
159    }
160    .bind_expr(expression, &BTreeSet::new())?;
161    Ok(RuleDependencies {
162        relations: dependencies,
163        columns: BTreeSet::new(),
164        routines: BTreeSet::new(),
165    })
166}
167
168pub fn bind_stored_statement_relations(
169    catalog: &dyn StoredRelationCatalog,
170    statement: &mut Statement,
171    lookup_mode: RelationLookupMode,
172    loaded_catalog: bool,
173    context: &str,
174) -> Result<bool, SQLError> {
175    let mut dependencies = BTreeSet::new();
176    let mut changed = false;
177    let mut bind = |reference: &mut String| {
178        let previous = reference.clone();
179        bind_catalog_relation_reference(
180            catalog,
181            reference,
182            lookup_mode,
183            loaded_catalog,
184            context,
185            &mut dependencies,
186        )?;
187        changed |= reference != &previous;
188        Ok(())
189    };
190    let mut ignore_routine = |_: &mut String,
191                              _: Option<&mut Option<crate::ast::FunctionBinding>>|
192     -> Result<(), SQLError> { Ok(()) };
193    StoredAstVisitor {
194        source: None,
195        merge: None,
196        expression: None,
197        ty: None,
198        relation: &mut bind,
199        routine: &mut ignore_routine,
200    }
201    .bind_statement(statement)?;
202    match statement {
203        Statement::Insert(insert) => {
204            changed |= !insert.target_relation_bound;
205            insert.target_relation_bound = true;
206        }
207        Statement::Update(update) => {
208            changed |= !update.target_relation_bound;
209            update.target_relation_bound = true;
210        }
211        Statement::Delete(delete) => {
212            changed |= !delete.target_relation_bound;
213            delete.target_relation_bound = true;
214        }
215        _ => {}
216    }
217    Ok(changed)
218}