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/// The usable namespace order and creation target of one analysis. An explicitly placed `pg_catalog` can leave the lookup order unchanged while changing the creation namespace, which `SearchPathMatchesCurrentEnvironment` also compares.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct EffectiveSearchPath {
15    pub schemas: Vec<String>,
16    pub creation_namespace: Option<String>,
17}
18
19/// Immutable session inputs used to resolve unqualified relation names during one statement.
20#[derive(Clone)]
21pub struct RelationNameResolution {
22    pub search_path: Vec<String>,
23    pub temporary_schema: String,
24    pub temporary_namespace_allocated: bool,
25    pub current_user: RoleReference,
26    pub lookup_mode: RelationLookupMode,
27}
28
29/// Whether a query resolves session-visible names or follows catalog identities captured when a stored expression was defined.
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub enum RelationLookupMode {
32    Dynamic,
33    Bound,
34}
35
36impl RelationNameResolution {
37    pub fn search_path(&self) -> &[String] {
38        &self.search_path
39    }
40
41    pub fn search_path_contains(&self, schema: &str) -> bool {
42        self.search_path.iter().any(|candidate| candidate == schema)
43    }
44
45    pub fn current_user(&self) -> &RoleReference {
46        &self.current_user
47    }
48
49    pub fn lookup_mode(&self) -> RelationLookupMode {
50        self.lookup_mode
51    }
52
53    pub fn qualified_schema(&self, name: &str) -> Result<Option<(String, String)>, SQLError> {
54        let (schema, _) = uqa_core::RelationIdentity::parse_reference(name).map_err(|error| {
55            SQLError::Internal(format!("resolve catalog relation `{name}`: {error}"))
56        })?;
57        Ok(schema.map(|schema| {
58            let resolved = if schema == "pg_temp" {
59                self.temporary_schema.clone()
60            } else {
61                schema.clone()
62            };
63            (schema, resolved)
64        }))
65    }
66
67    pub fn set_lookup_mode(&mut self, lookup_mode: RelationLookupMode) -> RelationLookupMode {
68        std::mem::replace(&mut self.lookup_mode, lookup_mode)
69    }
70
71    pub fn raw_relation_lookup_candidates(
72        &self,
73        name: &str,
74    ) -> Result<Vec<uqa_core::RelationIdentity>, SQLError> {
75        let (schema, relation) =
76            uqa_core::RelationIdentity::parse_reference(name).map_err(|error| {
77                SQLError::Internal(format!("resolve catalog relation `{name}`: {error}"))
78            })?;
79        if let Some(schema) = schema {
80            let schema = if schema == "pg_temp" {
81                self.temporary_schema.clone()
82            } else {
83                schema
84            };
85            return Ok(vec![uqa_core::RelationIdentity::new(schema, relation)]);
86        }
87        Ok(candidates::unqualified_candidates(
88            &self.temporary_schema,
89            &self.search_path,
90            &relation,
91        ))
92    }
93}
94
95/// Complete outcome of resolving one relation reference through a statement namespace.
96#[derive(Clone, Debug, Eq, PartialEq)]
97pub enum RelationResolution {
98    Found(String, &'static str),
99    MissingRelation,
100    MissingSchema(String),
101}
102
103impl RelationResolution {
104    /// Collapse namespace absence only for SQL boundaries whose contract reports an undefined relation for either absence outcome.
105    pub fn into_found(self) -> Option<(String, &'static str)> {
106        match self {
107            Self::Found(name, kind) => Some((name, kind)),
108            Self::MissingRelation | Self::MissingSchema(_) => None,
109        }
110    }
111}
112
113pub fn missing_relation_notice(name: &str) -> Result<String, crate::SQLError> {
114    let (_, local_name) =
115        uqa_core::RelationIdentity::parse_reference(name).map_err(crate::SQLError::Internal)?;
116    Ok(format!(
117        "relation \"{local_name}\" does not exist, skipping"
118    ))
119}
120
121/// Bind rename-source diagnostics without losing the distinction between missing schemas and relations.
122pub fn resolve_relation_rename_source(
123    resolution: RelationResolution,
124    name: &str,
125    if_exists: bool,
126    notice: &mut dyn FnMut(&str),
127) -> Result<Option<(String, &'static str)>, crate::SQLError> {
128    match resolution {
129        RelationResolution::Found(canonical, kind) => Ok(Some((canonical, kind))),
130        RelationResolution::MissingSchema(_) | RelationResolution::MissingRelation if if_exists => {
131            notice(&missing_relation_notice(name)?);
132            Ok(None)
133        }
134        RelationResolution::MissingSchema(schema) => Err(crate::SQLError::Routine {
135            sqlstate: "3F000".into(),
136            message: format!("schema \"{schema}\" does not exist"),
137        }),
138        RelationResolution::MissingRelation => Err(crate::SQLError::Routine {
139            sqlstate: "42P01".into(),
140            message: format!("relation \"{name}\" does not exist"),
141        }),
142    }
143}
144
145pub mod candidates;
146
147pub mod creation;
148
149/// The name `PostgreSQL`'s object descriptions give `relation`: its own name, quoted as an identifier needs, when the search path finds that relation by it, and its schema-qualified name otherwise.
150pub fn described_relation_name(
151    relation: &uqa_core::RelationIdentity,
152    resolve: impl FnOnce(&str) -> Result<RelationResolution, SQLError>,
153) -> Result<String, SQLError> {
154    let local = crate::expr::quote_ident(&relation.name);
155    let visible = match resolve(&local)? {
156        RelationResolution::Found(found, _) => uqa_core::RelationIdentity::from_legacy_name(&found)
157            .is_ok_and(|found| found == *relation),
158        RelationResolution::MissingRelation | RelationResolution::MissingSchema(_) => false,
159    };
160    Ok(if visible {
161        local
162    } else {
163        format!("{}.{local}", crate::expr::quote_ident(&relation.schema))
164    })
165}