Skip to main content

uqa_sql/ast/types/
identity.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! OID identities of user-defined types. Stored syntax, routine signatures and bound casts name a user-defined type by identity, so a rename, a schema move or a different search path cannot change which type they mean; output spells the type's current name. `#` cannot occur in an unquoted type name, and quoted names keep their quotes, so an identity never collides with a name as written.
8
9use super::ColumnType;
10
11const ENUM_PREFIX: &str = "enum#";
12const DOMAIN_PREFIX: &str = "domain#";
13const COMPOSITE_PREFIX: &str = "composite#";
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum UserTypeKind {
17    Enum,
18    Domain,
19    Composite,
20}
21
22/// A parsed identity: the type's kind and OID, and the number of array dimensions around it.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct UserTypeIdentity {
25    pub kind: UserTypeKind,
26    pub oid: u32,
27    pub dimensions: usize,
28}
29
30impl UserTypeIdentity {
31    /// Parse `enum#<oid>`, `domain#<oid>` or `composite#<oid>` with any number of `[]` suffixes.
32    #[must_use]
33    pub fn parse(name: &str) -> Option<Self> {
34        let mut element = name.trim();
35        let mut dimensions = 0usize;
36        while let Some(inner) = element.strip_suffix("[]") {
37            element = inner;
38            dimensions += 1;
39        }
40        let (kind, oid) = if let Some(oid) = element.strip_prefix(ENUM_PREFIX) {
41            (UserTypeKind::Enum, oid)
42        } else if let Some(oid) = element.strip_prefix(COMPOSITE_PREFIX) {
43            (UserTypeKind::Composite, oid)
44        } else {
45            (UserTypeKind::Domain, element.strip_prefix(DOMAIN_PREFIX)?)
46        };
47        if oid.is_empty() || !oid.bytes().all(|byte| byte.is_ascii_digit()) {
48            return None;
49        }
50        Some(Self {
51            kind,
52            oid: oid.parse().ok()?,
53            dimensions,
54        })
55    }
56}
57
58impl ColumnType {
59    /// The identity of a user-defined type or an array of one; built-in types have none.
60    #[must_use]
61    pub fn user_type_identity(&self) -> Option<String> {
62        match self {
63            ColumnType::Enum(reference) => Some(format!("{ENUM_PREFIX}{}", reference.oid)),
64            ColumnType::Composite(reference) => {
65                Some(format!("{COMPOSITE_PREFIX}{}", reference.oid))
66            }
67            ColumnType::Domain { oid, .. } => Some(format!("{DOMAIN_PREFIX}{oid}")),
68            ColumnType::Array(element) => element
69                .user_type_identity()
70                .map(|element| format!("{element}[]")),
71            _ => None,
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::{UserTypeIdentity, UserTypeKind};
79    use crate::ast::{ColumnType, EnumTypeReference};
80
81    #[test]
82    fn identities_round_trip_through_arrays() {
83        let mood = ColumnType::Enum(EnumTypeReference {
84            schema: "public".into(),
85            name: "mood".into(),
86            oid: 20_000,
87            array_oid: 20_001,
88        });
89        let array = ColumnType::Array(Box::new(ColumnType::Array(Box::new(mood))));
90        let identity = array.user_type_identity().unwrap();
91        assert_eq!(identity, "enum#20000[][]");
92        assert_eq!(
93            UserTypeIdentity::parse(&identity),
94            Some(UserTypeIdentity {
95                kind: UserTypeKind::Enum,
96                oid: 20_000,
97                dimensions: 2,
98            })
99        );
100        assert_eq!(
101            UserTypeIdentity::parse("composite#9[]"),
102            Some(UserTypeIdentity {
103                kind: UserTypeKind::Composite,
104                oid: 9,
105                dimensions: 1,
106            })
107        );
108        assert_eq!(
109            UserTypeIdentity::parse("domain#7"),
110            Some(UserTypeIdentity {
111                kind: UserTypeKind::Domain,
112                oid: 7,
113                dimensions: 0,
114            })
115        );
116        for name in ["\"enum#1\"", "enum#", "enum#x", "domain#-1", "integer"] {
117            assert_eq!(UserTypeIdentity::parse(name), None, "{name}");
118        }
119        assert_eq!(ColumnType::Integer.user_type_identity(), None);
120    }
121}