Skip to main content

uqa_sql/binding/stored_relations/
query.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Query relation and sequence binding against current or already-loaded catalog state.
8
9use super::StoredRelationCatalog;
10use crate::{
11    binding::view_dependencies::{bind_query_plan_relations, bind_query_plan_sequence_references},
12    catalog::resolution::RelationLookupMode,
13    plan::QueryPlan,
14    SQLError,
15};
16use std::collections::BTreeSet;
17use uqa_core::RelationIdentity;
18
19pub trait StoredQuerySequences {
20    fn query_sequence(&self, reference: &str) -> Result<String, String>;
21    fn loaded_query_sequence(&self, reference: &str) -> Result<String, String>;
22}
23/// Namespace metadata captured at the start of a stored query binding pass.
24pub struct StoredQueryNamespace {
25    pub temporary_schema: String,
26    pub transition_relations: BTreeSet<String>,
27}
28pub struct StoredQueryBindingContext<'a> {
29    pub relations: &'a dyn StoredRelationCatalog,
30    pub lookup_mode: RelationLookupMode,
31    pub sequences: &'a dyn StoredQuerySequences,
32    pub temporary_schema: &'a str,
33    pub transition_relations: &'a BTreeSet<String>,
34}
35
36pub fn resolve_loaded_query_sequence(
37    reference: &str,
38    candidates: Vec<RelationIdentity>,
39    mut contains: impl FnMut(&RelationIdentity) -> bool,
40) -> Result<String, String> {
41    candidates
42        .into_iter()
43        .find(|candidate| contains(candidate))
44        .map(|candidate| candidate.qualified_name())
45        .ok_or_else(|| format!("Sequence `{reference}` does not exist"))
46}
47
48pub fn bind_stored_query_relations(
49    catalog: &StoredQueryBindingContext<'_>,
50    plan: &mut QueryPlan,
51    context: &str,
52    reject_transition_relations: bool,
53    loaded_catalog: bool,
54) -> Result<bool, SQLError> {
55    let mut uses_temporary_relation = false;
56    bind_query_plan_relations(plan, &std::collections::BTreeSet::new(), &mut |reference| {
57        if catalog.lookup_mode == RelationLookupMode::Dynamic {
58            if let Some(canonical) = catalog
59                .relations
60                .resolve_age_label_relation_name(reference)?
61            {
62                return Ok(canonical);
63            }
64        }
65        if RelationIdentity::parse_reference(reference).is_ok_and(|(schema, relation)| {
66            schema.is_none() && catalog.transition_relations.contains(&relation)
67        }) {
68            if reject_transition_relations {
69                return Err(SQLError::Routine {
70                    sqlstate: "0A000".into(),
71                    message: "transition tables cannot be referenced in a view definition".into(),
72                });
73            }
74            return Ok(reference.to_string());
75        }
76        let resolved = match (catalog.lookup_mode, loaded_catalog) {
77            (RelationLookupMode::Bound, _) => {
78                catalog.relations.resolve_bound_relation_kind(reference)?
79            }
80            (RelationLookupMode::Dynamic, true) => catalog
81                .relations
82                .resolve_loaded_visible_relation_kind(reference)?,
83            (RelationLookupMode::Dynamic, false) => {
84                catalog.relations.resolve_visible_relation_kind(reference)?
85            }
86        }
87        .into_found();
88        match resolved {
89            Some((canonical, "table" | "view" | "materialized view" | "foreign table")) => {
90                uses_temporary_relation |= RelationIdentity::from_legacy_name(&canonical)
91                    .is_ok_and(|relation| relation.schema == catalog.temporary_schema);
92                Ok(canonical)
93            }
94            Some((canonical, "composite type")) => Err(composite_relation_error(&canonical)),
95            Some((canonical, kind)) => Err(SQLError::Routine {
96                sqlstate: "42809".into(),
97                message: format!(
98                    "{context} source \"{canonical}\" is a {kind}, not a row relation"
99                ),
100            }),
101            None => Err(SQLError::UnknownTable(reference.to_string())),
102        }
103    })?;
104    bind_query_plan_sequence_references(plan, &mut |reference| {
105        let resolved = if loaded_catalog {
106            catalog.sequences.loaded_query_sequence(reference)
107        } else {
108            catalog.sequences.query_sequence(reference)
109        };
110        resolved.map_err(|error| {
111            SQLError::Unsupported(format!(
112                "{context} sequence reference `{reference}`: {error}"
113            ))
114        })
115    })?;
116    Ok(uses_temporary_relation)
117}
118
119/// A composite type's relation holds no rows, so `table_open` refuses it.
120pub(super) fn composite_relation_error(canonical: &str) -> SQLError {
121    crate::catalog::analysis::UnopenableRelation {
122        name: RelationIdentity::from_legacy_name(canonical)
123            .map_or_else(|_| canonical.to_string(), |relation| relation.name),
124        kinds: "composite types",
125    }
126    .error()
127}
128
129#[cfg(test)]
130mod tests;