Skip to main content

uqa_sql/catalog/
resolution.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Relation namespace inputs for static SQL analysis.
8
9use crate::SQLError;
10
11/// Immutable session inputs used to resolve unqualified relation names during one statement.
12#[derive(Clone)]
13pub struct RelationNameResolution {
14    pub search_path: Vec<String>,
15    pub temporary_schema: String,
16    pub temporary_namespace_allocated: bool,
17    pub current_user: String,
18    pub lookup_mode: RelationLookupMode,
19}
20
21/// Whether a query resolves session-visible names or follows catalog identities captured when a stored expression was defined.
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum RelationLookupMode {
24    Dynamic,
25    Bound,
26}
27
28impl RelationNameResolution {
29    pub fn search_path(&self) -> &[String] {
30        &self.search_path
31    }
32
33    pub fn search_path_contains(&self, schema: &str) -> bool {
34        self.search_path.iter().any(|candidate| candidate == schema)
35    }
36
37    pub fn current_user(&self) -> &str {
38        &self.current_user
39    }
40
41    pub fn lookup_mode(&self) -> RelationLookupMode {
42        self.lookup_mode
43    }
44
45    pub fn qualified_schema(&self, name: &str) -> Result<Option<(String, String)>, SQLError> {
46        let (schema, _) = uqa_core::RelationIdentity::parse_reference(name).map_err(|error| {
47            SQLError::Internal(format!("resolve catalog relation `{name}`: {error}"))
48        })?;
49        Ok(schema.map(|schema| {
50            let resolved = if schema == "pg_temp" {
51                self.temporary_schema.clone()
52            } else {
53                schema.clone()
54            };
55            (schema, resolved)
56        }))
57    }
58
59    pub fn set_lookup_mode(&mut self, lookup_mode: RelationLookupMode) -> RelationLookupMode {
60        std::mem::replace(&mut self.lookup_mode, lookup_mode)
61    }
62
63    pub fn raw_relation_lookup_candidates(
64        &self,
65        name: &str,
66    ) -> Result<Vec<uqa_core::RelationIdentity>, SQLError> {
67        let (schema, relation) =
68            uqa_core::RelationIdentity::parse_reference(name).map_err(|error| {
69                SQLError::Internal(format!("resolve catalog relation `{name}`: {error}"))
70            })?;
71        if let Some(schema) = schema {
72            let schema = if schema == "pg_temp" {
73                self.temporary_schema.clone()
74            } else {
75                schema
76            };
77            return Ok(vec![uqa_core::RelationIdentity::new(schema, relation)]);
78        }
79        let mut candidates = vec![uqa_core::RelationIdentity::new(
80            &self.temporary_schema,
81            &relation,
82        )];
83        candidates.extend(
84            self.search_path
85                .iter()
86                .filter(|schema| *schema != "pg_catalog" && *schema != "information_schema")
87                .map(|schema| uqa_core::RelationIdentity::new(schema, &relation)),
88        );
89        Ok(candidates)
90    }
91}
92
93/// Complete outcome of resolving one relation reference through a statement namespace.
94#[derive(Clone, Debug, Eq, PartialEq)]
95pub enum RelationResolution {
96    Found(String, &'static str),
97    MissingRelation,
98    MissingSchema(String),
99}
100
101impl RelationResolution {
102    /// Collapse namespace absence only for SQL boundaries whose contract reports an undefined relation for either absence outcome.
103    pub fn into_found(self) -> Option<(String, &'static str)> {
104        match self {
105            Self::Found(name, kind) => Some((name, kind)),
106            Self::MissingRelation | Self::MissingSchema(_) => None,
107        }
108    }
109}
110
111/// Bind rename-source diagnostics without losing the distinction between missing schemas and relations.
112pub fn resolve_relation_rename_source(
113    resolution: RelationResolution,
114    name: &str,
115    if_exists: bool,
116    notice: &mut dyn FnMut(&str),
117) -> Result<Option<(String, &'static str)>, crate::SQLError> {
118    match resolution {
119        RelationResolution::Found(canonical, kind) => Ok(Some((canonical, kind))),
120        RelationResolution::MissingSchema(_) | RelationResolution::MissingRelation if if_exists => {
121            let (_, local_name) = uqa_core::RelationIdentity::parse_reference(name)
122                .map_err(crate::SQLError::Internal)?;
123            notice(&format!(
124                "relation \"{local_name}\" does not exist, skipping"
125            ));
126            Ok(None)
127        }
128        RelationResolution::MissingSchema(schema) => Err(crate::SQLError::Routine {
129            sqlstate: "3F000".into(),
130            message: format!("schema \"{schema}\" does not exist"),
131        }),
132        RelationResolution::MissingRelation => Err(crate::SQLError::Routine {
133            sqlstate: "42P01".into(),
134            message: format!("relation \"{name}\" does not exist"),
135        }),
136    }
137}
138
139pub mod candidates;
140
141pub mod creation;