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, "composite type") => {
71            return Err(query::composite_relation_error(&canonical));
72        }
73        RelationResolution::Found(canonical, kind) => {
74            return Err(SQLError::Routine {
75                sqlstate: "42809".into(),
76                message: format!(
77                    "{context} source \"{canonical}\" is a {kind}, not a row relation"
78                ),
79            });
80        }
81        RelationResolution::MissingSchema(schema) => {
82            return Err(SQLError::Routine {
83                sqlstate: "3F000".into(),
84                message: format!("schema \"{schema}\" does not exist"),
85            });
86        }
87        RelationResolution::MissingRelation => {
88            return Err(SQLError::UnknownTable(reference.clone()));
89        }
90    };
91    let relation = RelationIdentity::from_legacy_name(&canonical).map_err(|error| {
92        SQLError::Internal(format!("decode bound rule source `{canonical}`: {error}"))
93    })?;
94    *reference = canonical;
95    if crate::catalog::VirtualRelation::at(&relation.schema, &relation.name).is_none() {
96        dependencies.insert(relation);
97    }
98    Ok(())
99}
100
101pub fn bind_rule_action_relation_dependencies(
102    catalog: &dyn StoredRelationCatalog,
103    statement: &mut Statement,
104    lookup_mode: RelationLookupMode,
105) -> Result<RuleDependencies, SQLError> {
106    let mut dependencies = BTreeSet::new();
107    let mut bind = |reference: &mut String| {
108        bind_catalog_relation_reference(
109            catalog,
110            reference,
111            lookup_mode,
112            false,
113            "CREATE RULE",
114            &mut dependencies,
115        )
116    };
117    let mut ignore_routine = |_: &mut String,
118                              _: Option<&mut Option<crate::ast::FunctionBinding>>|
119     -> Result<(), SQLError> { Ok(()) };
120    StoredAstVisitor {
121        source: None,
122        merge: None,
123        expression: None,
124        projection: None,
125        ty: None,
126        relation: &mut bind,
127        routine: &mut ignore_routine,
128    }
129    .bind_statement(statement)?;
130    Ok(RuleDependencies {
131        relations: dependencies,
132        columns: BTreeSet::new(),
133        routines: BTreeSet::new(),
134    })
135}
136
137pub fn bind_rule_condition_relation_dependencies(
138    catalog: &dyn StoredRelationCatalog,
139    expression: &mut Expr,
140    lookup_mode: RelationLookupMode,
141) -> Result<RuleDependencies, SQLError> {
142    let mut dependencies = BTreeSet::new();
143    let mut bind = |reference: &mut String| {
144        bind_catalog_relation_reference(
145            catalog,
146            reference,
147            lookup_mode,
148            false,
149            "CREATE RULE",
150            &mut dependencies,
151        )
152    };
153    let mut ignore_routine = |_: &mut String,
154                              _: Option<&mut Option<crate::ast::FunctionBinding>>|
155     -> Result<(), SQLError> { Ok(()) };
156    StoredAstVisitor {
157        source: None,
158        merge: None,
159        expression: None,
160        projection: None,
161        ty: None,
162        relation: &mut bind,
163        routine: &mut ignore_routine,
164    }
165    .bind_expr(expression, &BTreeSet::new())?;
166    Ok(RuleDependencies {
167        relations: dependencies,
168        columns: BTreeSet::new(),
169        routines: BTreeSet::new(),
170    })
171}
172
173pub fn bind_stored_statement_relations(
174    catalog: &dyn StoredRelationCatalog,
175    statement: &mut Statement,
176    lookup_mode: RelationLookupMode,
177    loaded_catalog: bool,
178    context: &str,
179) -> Result<bool, SQLError> {
180    let mut dependencies = BTreeSet::new();
181    let mut changed = false;
182    let mut bind = |reference: &mut String| {
183        let previous = reference.clone();
184        bind_catalog_relation_reference(
185            catalog,
186            reference,
187            lookup_mode,
188            loaded_catalog,
189            context,
190            &mut dependencies,
191        )?;
192        changed |= reference != &previous;
193        Ok(())
194    };
195    let mut ignore_routine = |_: &mut String,
196                              _: Option<&mut Option<crate::ast::FunctionBinding>>|
197     -> Result<(), SQLError> { Ok(()) };
198    StoredAstVisitor {
199        source: None,
200        merge: None,
201        expression: None,
202        projection: None,
203        ty: None,
204        relation: &mut bind,
205        routine: &mut ignore_routine,
206    }
207    .bind_statement(statement)?;
208    match statement {
209        Statement::Insert(insert) => {
210            changed |= !insert.target_relation_bound;
211            insert.target_relation_bound = true;
212        }
213        Statement::Update(update) => {
214            changed |= !update.target_relation_bound;
215            update.target_relation_bound = true;
216        }
217        Statement::Delete(delete) => {
218            changed |= !delete.target_relation_bound;
219            delete.target_relation_bound = true;
220        }
221        _ => {}
222    }
223    Ok(changed)
224}