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    pub identity: RelationIdentity,
24    pub owner: Owner,
25    pub definition: CreateDomain,
26}
27
28impl<Owner> StoredDomain<Owner> {
29    pub fn column_type(&self) -> ColumnType {
30        ColumnType::Domain {
31            schema: self.identity.schema.clone(),
32            name: self.identity.name.clone(),
33            oid: self.oid,
34            base: Box::new(self.definition.base.clone()),
35        }
36    }
37}
38
39impl StoredDomain<String> {
40    /// Bind a legacy name only when the catalog restoration owner allows conversion.
41    pub fn bind_owner(
42        self,
43        roles: &BTreeMap<String, RoleDefinition>,
44    ) -> Result<StoredDomain, String> {
45        let owner = RoleReference::Named(self.owner)
46            .bind(roles)
47            .map_err(|error| error.to_string())?
48            .identity();
49        Ok(StoredDomain {
50            object_id: self.object_id,
51            oid: self.oid,
52            identity: self.identity,
53            owner,
54            definition: self.definition,
55        })
56    }
57}
58
59/// Validate the complete catalog before any durable conversion or registry publication.
60pub fn validate_domain_registry(
61    registry: &BTreeMap<String, StoredDomain>,
62    roles: &BTreeMap<String, RoleDefinition>,
63) -> Result<(), String> {
64    validate_domain_definitions(registry, roles)?;
65    for domain in registry.values() {
66        crate::schema::domains::constraints::validate(&domain.definition, false)
67            .map_err(|error| error.to_string())?;
68    }
69    Ok(())
70}
71
72/// Early restoration validates authority and supplied identities before any missing legacy constraint metadata is allocated.
73pub fn validate_domain_definitions(
74    registry: &BTreeMap<String, StoredDomain>,
75    roles: &BTreeMap<String, RoleDefinition>,
76) -> Result<(), String> {
77    let mut identities = BTreeSet::new();
78    let mut oids = BTreeSet::new();
79    let mut constraint_objects = BTreeSet::new();
80    let mut constraint_oids = BTreeSet::new();
81    for (name, domain) in registry {
82        if domain.object_id == [0; 16]
83            || !identities.insert(domain.object_id)
84            || domain.oid != domain_object_oid(&domain.object_id)
85            || !oids.insert(domain.oid)
86        {
87            return Err(format!("invalid or duplicate domain identity for `{name}`"));
88        }
89        if domain.identity.schema.is_empty()
90            || domain.identity.name.is_empty()
91            || domain.identity.qualified_name() != *name
92            || RelationIdentity::from_legacy_name(&domain.definition.name).as_ref()
93                != Ok(&domain.identity)
94        {
95            return Err(format!("inconsistent domain name for `{name}`"));
96        }
97        if !domain.owner.is_valid() || domain.owner.role_definition(roles).is_none() {
98            return Err(format!(
99                "domain `{name}` references missing role incarnation {}",
100                domain.owner.oid
101            ));
102        }
103        crate::schema::domains::constraints::validate(&domain.definition, true)
104            .map_err(|error| error.to_string())?;
105        for identity in crate::schema::domains::constraints::identities(&domain.definition) {
106            if !constraint_objects.insert(identity.object_id)
107                || !constraint_oids.insert(identity.oid)
108            {
109                return Err(format!(
110                    "duplicate domain constraint catalog identity for `{name}`"
111                ));
112            }
113        }
114    }
115    Ok(())
116}
117
118/// Definition lookup for domain inheritance and constraint binding.
119pub trait DomainCatalog {
120    fn domain_by_oid(&self, oid: u32) -> Option<StoredDomain>;
121}
122
123pub fn domain_default_expression(
124    catalog: &dyn DomainCatalog,
125    ty: &crate::ColumnType,
126) -> Option<crate::ast::Expr> {
127    let crate::ColumnType::Domain { oid, base, .. } = ty else {
128        return None;
129    };
130    catalog
131        .domain_by_oid(*oid)
132        .and_then(|domain| domain.definition.default)
133        .or_else(|| domain_default_expression(catalog, base))
134}
135
136#[cfg(test)]
137mod tests;