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            base: Box::new(self.definition.base.clone()),
45        }
46    }
47
48    /// The name of the domain's generated array type.
49    pub fn array_type_name(&self) -> String {
50        self.array_name
51            .clone()
52            .unwrap_or_else(|| super::array_type_names::array_type_name(&self.identity.name, 0))
53    }
54}
55
56impl StoredDomain<String> {
57    /// Bind a legacy name only when the catalog restoration owner allows conversion.
58    pub fn bind_owner(
59        self,
60        roles: &BTreeMap<String, RoleDefinition>,
61    ) -> Result<StoredDomain, String> {
62        let owner = RoleReference::Named(self.owner)
63            .bind(roles)
64            .map_err(|error| error.to_string())?
65            .identity();
66        Ok(StoredDomain {
67            object_id: self.object_id,
68            oid: self.oid,
69            array_oid: self.array_oid,
70            identity: self.identity,
71            owner,
72            definition: self.definition,
73            array_name: self.array_name,
74            usage_acl: self.usage_acl,
75        })
76    }
77}
78
79/// Validate the complete catalog before any durable conversion or registry publication.
80pub fn validate_domain_registry(
81    registry: &BTreeMap<String, StoredDomain>,
82    roles: &BTreeMap<String, RoleDefinition>,
83) -> Result<(), String> {
84    validate_domain_definitions(registry, roles)?;
85    for domain in registry.values() {
86        crate::schema::domains::constraints::validate(&domain.definition, false)
87            .map_err(|error| error.to_string())?;
88    }
89    Ok(())
90}
91
92/// Early restoration validates authority and supplied identities before any missing legacy constraint metadata is allocated.
93pub fn validate_domain_definitions(
94    registry: &BTreeMap<String, StoredDomain>,
95    roles: &BTreeMap<String, RoleDefinition>,
96) -> Result<(), String> {
97    let mut identities = BTreeSet::new();
98    let mut oids = BTreeSet::new();
99    let mut constraint_objects = BTreeSet::new();
100    let mut constraint_oids = BTreeSet::new();
101    for (name, domain) in registry {
102        // A domain with a recorded array OID drew both OIDs from the database's counter; an earlier one derives its OID from its identity.
103        let oids_valid = match domain.array_oid {
104            Some(array_oid) => {
105                domain.oid >= super::oids::FIRST_NORMAL_OBJECT_ID
106                    && array_oid >= super::oids::FIRST_NORMAL_OBJECT_ID
107                    && oids.insert(array_oid)
108            }
109            None => domain.oid == domain_object_oid(&domain.object_id),
110        };
111        if domain.object_id == [0; 16]
112            || !identities.insert(domain.object_id)
113            || !oids_valid
114            || !oids.insert(domain.oid)
115        {
116            return Err(format!("invalid or duplicate domain identity for `{name}`"));
117        }
118        if domain.identity.schema.is_empty()
119            || domain.identity.name.is_empty()
120            || domain.identity.qualified_name() != *name
121            || RelationIdentity::from_legacy_name(&domain.definition.name).as_ref()
122                != Ok(&domain.identity)
123        {
124            return Err(format!("inconsistent domain name for `{name}`"));
125        }
126        if !domain.owner.is_valid() || domain.owner.role_definition(roles).is_none() {
127            return Err(format!(
128                "domain `{name}` references missing role incarnation {}",
129                domain.owner.oid
130            ));
131        }
132        crate::schema::domains::constraints::validate(&domain.definition, true)
133            .map_err(|error| error.to_string())?;
134        for identity in crate::schema::domains::constraints::identities(&domain.definition) {
135            if !constraint_objects.insert(identity.object_id)
136                || !constraint_oids.insert(identity.oid)
137            {
138                return Err(format!(
139                    "duplicate domain constraint catalog identity for `{name}`"
140                ));
141            }
142        }
143    }
144    Ok(())
145}
146
147/// Definition lookup for domain inheritance and constraint binding.
148pub trait DomainCatalog {
149    fn domain_by_oid(&self, oid: u32) -> Option<StoredDomain>;
150}
151
152pub fn domain_default_expression(
153    catalog: &dyn DomainCatalog,
154    ty: &crate::ColumnType,
155) -> Option<crate::ast::Expr> {
156    let crate::ColumnType::Domain { oid, base, .. } = ty else {
157        return None;
158    };
159    catalog
160        .domain_by_oid(*oid)
161        .and_then(|domain| domain.definition.default)
162        .or_else(|| domain_default_expression(catalog, base))
163}
164
165#[cfg(test)]
166mod tests;