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