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;
11
12use super::ColumnType;
13
14thread_local! {
15    static SEARCH_PATH: RefCell<Option<Arc<[String]>>> = const { RefCell::new(None) };
16}
17
18/// 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.
19pub struct TypeDisplayScope {
20    previous: Option<Arc<[String]>>,
21}
22
23impl TypeDisplayScope {
24    #[must_use]
25    pub fn enter(search_path: &[String]) -> Self {
26        let previous = SEARCH_PATH.with(|current| current.replace(Some(search_path.into())));
27        Self { previous }
28    }
29
30    /// Record the search path of the next statement in the same scope, which an earlier statement may have changed.
31    pub fn refresh(&self, search_path: &[String]) {
32        SEARCH_PATH.with(|current| {
33            current.replace(Some(search_path.into()));
34        });
35    }
36}
37
38impl Drop for TypeDisplayScope {
39    fn drop(&mut self) {
40        SEARCH_PATH.with(|current| {
41            current.replace(self.previous.take());
42        });
43    }
44}
45
46fn schema_visible(schema: &str) -> bool {
47    schema == "pg_catalog"
48        || SEARCH_PATH.with(|current| {
49            current.borrow().as_ref().map_or_else(
50                || schema == "public",
51                |path| path.iter().any(|entry| entry == schema),
52            )
53        })
54}
55
56/// A user-defined type's name, qualified by its schema when the schema is not visible.
57pub(super) fn visible_type_name(schema: &str, name: &str) -> String {
58    let local = crate::expr::quote_ident(name);
59    if schema_visible(schema) {
60        local
61    } else {
62        format!("{}.{local}", crate::expr::quote_ident(schema))
63    }
64}
65
66impl ColumnType {
67    /// `format_type_be` of this type for a diagnostic.
68    #[must_use]
69    pub fn display_name(&self) -> String {
70        match self {
71            ColumnType::Enum(reference) => visible_type_name(&reference.schema, &reference.name),
72            ColumnType::Composite(reference) => {
73                visible_type_name(&reference.schema, &reference.name)
74            }
75            ColumnType::Domain { schema, name, .. } => visible_type_name(schema, name),
76            // An array type of any dimensionality is the element's one array type.
77            ColumnType::Array(element) => {
78                let mut leaf = element.as_ref();
79                while let ColumnType::Array(inner) = leaf {
80                    leaf = inner;
81                }
82                if matches!(
83                    leaf,
84                    ColumnType::Enum(_) | ColumnType::Composite(_) | ColumnType::Domain { .. }
85                ) {
86                    format!("{}[]", leaf.display_name())
87                } else {
88                    self.regtype_name()
89                }
90            }
91            other => other.regtype_name(),
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::TypeDisplayScope;
99    use crate::ast::{ColumnType, EnumTypeReference};
100
101    #[test]
102    fn hidden_user_types_are_qualified_in_diagnostics() {
103        let mood = ColumnType::Enum(EnumTypeReference {
104            schema: "hidden".into(),
105            name: "hmood".into(),
106            oid: 1,
107            array_oid: 2,
108        });
109        // Without a statement, the default search path applies.
110        assert_eq!(mood.display_name(), "hidden.hmood");
111        let scope = TypeDisplayScope::enter(&["public".into()]);
112        assert_eq!(mood.display_name(), "hidden.hmood");
113        assert_eq!(
114            ColumnType::Array(Box::new(mood.clone())).display_name(),
115            "hidden.hmood[]"
116        );
117        scope.refresh(&["hidden".into(), "public".into()]);
118        assert_eq!(mood.display_name(), "hmood");
119        drop(scope);
120        assert_eq!(mood.display_name(), "hidden.hmood");
121        assert_eq!(ColumnType::Integer.display_name(), "integer");
122    }
123}