Skip to main content

uqa_sql/ast/types/
display.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Type names in diagnostics, as `format_type_be` spells them: a user-defined type is qualified by its schema when the running statement's search path does not include that schema. The statement executor records the search path for the statement it runs on this thread; without a recorded path, `PostgreSQL`'s default search path applies.
8
9use std::cell::RefCell;
10use std::sync::Arc;
11use uqa_core::{
12    memory::{Produced, ProductionControl},
13    ValueRetentionError,
14};
15
16use super::ColumnType;
17
18thread_local! {
19    static SEARCH_PATH: RefCell<Option<Arc<[String]>>> = const { RefCell::new(None) };
20}
21
22/// The search path diagnostics use while a statement runs on this thread. Dropping the scope restores the enclosing statement's path, so nested SQL keeps its own.
23pub struct TypeDisplayScope {
24    previous: Option<Arc<[String]>>,
25}
26
27impl TypeDisplayScope {
28    #[must_use]
29    pub fn enter(search_path: &[String]) -> Self {
30        let previous = SEARCH_PATH.with(|current| current.replace(Some(search_path.into())));
31        Self { previous }
32    }
33
34    /// Record the search path of the next statement in the same scope, which an earlier statement may have changed.
35    pub fn refresh(&self, search_path: &[String]) {
36        SEARCH_PATH.with(|current| {
37            current.replace(Some(search_path.into()));
38        });
39    }
40}
41
42impl Drop for TypeDisplayScope {
43    fn drop(&mut self) {
44        SEARCH_PATH.with(|current| {
45            current.replace(self.previous.take());
46        });
47    }
48}
49
50fn schema_visible(schema: &str) -> bool {
51    schema == "pg_catalog"
52        || SEARCH_PATH.with(|current| {
53            current.borrow().as_ref().map_or_else(
54                || schema == "public",
55                |path| path.iter().any(|entry| entry == schema),
56            )
57        })
58}
59
60/// A user-defined type's name, qualified by its schema when the schema is not visible.
61pub(super) fn visible_type_name(schema: &str, name: &str) -> String {
62    visible_type_name_with_control(schema, name, &ProductionControl::uncontrolled())
63        .expect("ordinary visible type name")
64        .into_uncontrolled()
65        .expect("ordinary type name has no reservation")
66}
67
68fn visible_type_name_with_control(
69    schema: &str,
70    name: &str,
71    control: &ProductionControl<'_>,
72) -> Result<Produced<String>, ValueRetentionError> {
73    let local = crate::expr::quote_ident_with_control(name, control)?;
74    if schema_visible(schema) {
75        Ok(local)
76    } else {
77        let schema = crate::expr::quote_ident_with_control(schema, control)?;
78        control.format(format_args!("{}.{}", schema.as_str(), local.as_str()))
79    }
80}
81
82impl ColumnType {
83    /// `format_type_be` of this type for a diagnostic.
84    #[must_use]
85    pub fn display_name(&self) -> String {
86        self.display_name_with_control(&ProductionControl::uncontrolled())
87            .expect("ordinary diagnostic type name")
88            .into_uncontrolled()
89            .expect("ordinary type name has no reservation")
90    }
91
92    pub(crate) fn display_name_with_control(
93        &self,
94        control: &ProductionControl<'_>,
95    ) -> Result<Produced<String>, ValueRetentionError> {
96        control.check()?;
97        match self {
98            ColumnType::Enum(reference) => {
99                visible_type_name_with_control(&reference.schema, &reference.name, control)
100            }
101            ColumnType::Composite(reference) => {
102                visible_type_name_with_control(&reference.schema, &reference.name, control)
103            }
104            ColumnType::Domain { schema, name, .. } => {
105                visible_type_name_with_control(schema, name, control)
106            }
107            // An array type of any dimensionality is the element's one array type.
108            ColumnType::Array(element) => {
109                let mut leaf = element.as_ref();
110                while let ColumnType::Array(inner) = leaf {
111                    control.check()?;
112                    leaf = inner;
113                }
114                if matches!(
115                    leaf,
116                    ColumnType::Enum(_) | ColumnType::Composite(_) | ColumnType::Domain { .. }
117                ) {
118                    let leaf = leaf.display_name_with_control(control)?;
119                    control.format(format_args!("{}[]", leaf.as_str()))
120                } else {
121                    self.regtype_name_with_control(control)
122                }
123            }
124            other => other.regtype_name_with_control(control),
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::TypeDisplayScope;
132    use crate::ast::{ColumnType, EnumTypeReference};
133
134    #[test]
135    fn hidden_user_types_are_qualified_in_diagnostics() {
136        let mood = ColumnType::Enum(EnumTypeReference {
137            schema: "hidden".into(),
138            name: "hmood".into(),
139            oid: 1,
140            array_oid: 2,
141        });
142        // Without a statement, the default search path applies.
143        assert_eq!(mood.display_name(), "hidden.hmood");
144        let scope = TypeDisplayScope::enter(&["public".into()]);
145        assert_eq!(mood.display_name(), "hidden.hmood");
146        assert_eq!(
147            ColumnType::Array(Box::new(mood.clone())).display_name(),
148            "hidden.hmood[]"
149        );
150        scope.refresh(&["hidden".into(), "public".into()]);
151        assert_eq!(mood.display_name(), "hmood");
152        drop(scope);
153        assert_eq!(mood.display_name(), "hidden.hmood");
154        assert_eq!(ColumnType::Integer.display_name(), "integer");
155    }
156}