Skip to main content

uqa_sql/catalog/
domain.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7use crate::ast::{ColumnType, CreateDomain};
8use serde::{Deserialize, Serialize};
9use std::collections::{BTreeMap, BTreeSet};
10use uqa_core::{catalog_role::RoleIdentity, RelationIdentity};
11
12use super::roles::{identity::RoleSubject, RoleDefinition, RoleReference};
13
14pub fn domain_object_oid(object_id: &[u8; 16]) -> u32 {
15    u32::try_from(super::oids::stable_object_oid("domain", object_id))
16        .expect("catalog OIDs fit in u32")
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct StoredDomain<Owner = RoleIdentity> {
21    pub object_id: [u8; 16],
22    pub oid: u32,
23    /// The OID of the generated array type, allocated before the domain's own as `DefineDomain` does. Domains created before array OIDs were recorded derive both OIDs from their identity.
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub array_oid: Option<u32>,
26    pub identity: RelationIdentity,
27    pub owner: Owner,
28    pub definition: CreateDomain,
29    /// Name of the generated array type, chosen as `makeArrayTypeName` chooses it. Domains created before array names were recorded use the uncontested name.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub array_name: Option<String>,
32    /// Explicit `USAGE` privileges; `None` is the default ACL (PUBLIC and the owner).
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub usage_acl: Option<Vec<crate::ast::ObjectAclEntry>>,
35}
36
37impl<Owner> StoredDomain<Owner> {
38    pub fn column_type(&self) -> ColumnType {
39        ColumnType::Domain {
40            schema: self.identity.schema.clone(),
41            name: self.identity.name.clone(),
42            oid: self.oid,
43            array_oid: self.array_oid,
44            collation_oid: self.definition.collation_oid,
45            base: Box::new(self.definition.base.clone()),
46        }
47    }
48
49    /// The name of the domain's generated array type.
50    pub fn array_type_name(&self) -> String {
51        self.array_name
52            .clone()
53            .unwrap_or_else(|| super::array_type_names::array_type_name(&self.identity.name, 0))
54    }
55}
56
57impl StoredDomain<String> {
58    /// Bind a legacy name only when the catalog restoration owner allows conversion.
59    pub fn bind_owner(
60        self,
61        roles: &BTreeMap<String, RoleDefinition>,
62    ) -> Result<StoredDomain, String> {
63        let owner = RoleReference::Named(self.owner)
64            .bind(roles)
65            .map_err(|error| error.to_string())?
66            .identity();
67        Ok(StoredDomain {
68            object_id: self.object_id,
69            oid: self.oid,
70            array_oid: self.array_oid,
71            identity: self.identity,
72            owner,
73            definition: self.definition,
74            array_name: self.array_name,
75            usage_acl: self.usage_acl,
76        })
77    }
78}
79
80/// Validate the complete catalog before any durable conversion or registry publication.
81pub fn validate_domain_registry(
82    registry: &BTreeMap<String, StoredDomain>,
83    roles: &BTreeMap<String, RoleDefinition>,
84) -> Result<(), String> {
85    validate_domain_definitions(registry, roles)?;
86    for domain in registry.values() {
87        crate::schema::domains::constraints::validate(&domain.definition, false)
88            .map_err(|error| error.to_string())?;
89    }
90    Ok(())
91}
92
93/// Early restoration validates authority and supplied identities before any missing legacy constraint metadata is allocated.
94pub fn validate_domain_definitions(
95    registry: &BTreeMap<String, StoredDomain>,
96    roles: &BTreeMap<String, RoleDefinition>,
97) -> Result<(), String> {
98    let mut identities = BTreeSet::new();
99    let mut oids = BTreeSet::new();
100    let mut constraint_objects = BTreeSet::new();
101    let mut constraint_oids = BTreeSet::new();
102    for (name, domain) in registry {
103        // A domain with a recorded array OID drew both OIDs from the database's counter; an earlier one derives its OID from its identity.
104        let oids_valid = match domain.array_oid {
105            Some(array_oid) => {
106                domain.oid >= super::oids::FIRST_NORMAL_OBJECT_ID
107                    && array_oid >= super::oids::FIRST_NORMAL_OBJECT_ID
108                    && oids.insert(array_oid)
109            }
110            None => domain.oid == domain_object_oid(&domain.object_id),
111        };
112        if domain.object_id == [0; 16]
113            || !identities.insert(domain.object_id)
114            || !oids_valid
115            || !oids.insert(domain.oid)
116        {
117            return Err(format!("invalid or duplicate domain identity for `{name}`"));
118        }
119        if domain.identity.schema.is_empty()
120            || domain.identity.name.is_empty()
121            || domain.identity.qualified_name() != *name
122            || RelationIdentity::from_legacy_name(&domain.definition.name).as_ref()
123                != Ok(&domain.identity)
124        {
125            return Err(format!("inconsistent domain name for `{name}`"));
126        }
127        if !domain.owner.is_valid() || domain.owner.role_definition(roles).is_none() {
128            return Err(format!(
129                "domain `{name}` references missing role incarnation {}",
130                domain.owner.oid
131            ));
132        }
133        crate::schema::domains::constraints::validate(&domain.definition, true)
134            .map_err(|error| error.to_string())?;
135        for identity in crate::schema::domains::constraints::identities(&domain.definition) {
136            if !constraint_objects.insert(identity.object_id)
137                || !constraint_oids.insert(identity.oid)
138            {
139                return Err(format!(
140                    "duplicate domain constraint catalog identity for `{name}`"
141                ));
142            }
143        }
144    }
145    Ok(())
146}
147
148/// Definition lookup for domain inheritance and constraint binding.
149pub trait DomainCatalog {
150    fn domain_by_oid(&self, oid: u32) -> Option<StoredDomain>;
151}
152
153pub fn domain_default_expression(
154    catalog: &dyn DomainCatalog,
155    ty: &crate::ColumnType,
156) -> Option<crate::ast::Expr> {
157    let crate::ColumnType::Domain { oid, base, .. } = ty else {
158        return None;
159    };
160    catalog
161        .domain_by_oid(*oid)
162        .and_then(|domain| domain.definition.default)
163        .or_else(|| domain_default_expression(catalog, base))
164}
165
166#[cfg(test)]
167mod tests;