Skip to main content

uqa_core/
catalog_schema.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Durable schema ownership and access-control metadata shared by catalog consumers.
8
9use crate::catalog_role::{BoundAclEntry, RoleIdentity};
10use serde::{Deserialize, Serialize};
11
12/// A namespace lifetime and one replacement of its catalog tuple.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct SchemaTupleIdentity {
16    pub oid: i64,
17    pub object_id: [u8; 16],
18    pub revision: [u8; 16],
19}
20
21impl SchemaTupleIdentity {
22    pub fn is_valid(self) -> bool {
23        u32::try_from(self.oid).is_ok_and(|oid| oid != 0)
24            && self.object_id != [0; 16]
25            && self.revision != [0; 16]
26    }
27
28    /// Bootstrap and migrated namespaces retain their original OIDs with database-scoped identities.
29    pub fn initial(oid: u32) -> Self {
30        let mut object_id = [0; 16];
31        object_id[..4].copy_from_slice(&2615_u32.to_be_bytes());
32        object_id[12..].copy_from_slice(&oid.to_be_bytes());
33        Self {
34            oid: i64::from(oid),
35            object_id,
36            revision: object_id,
37        }
38    }
39}
40
41/// Schema security with role incarnations, independent of current display names.
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43pub struct BoundSchemaRow {
44    pub name: String,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub tuple: Option<SchemaTupleIdentity>,
47    pub role_owner: RoleIdentity,
48    pub acl: Option<Vec<BoundAclEntry<SchemaPrivileges>>>,
49}
50
51impl BoundSchemaRow {
52    pub fn bootstrap(name: impl Into<String>) -> Self {
53        let name = name.into();
54        let owner = RoleIdentity::BOOTSTRAP;
55        let acl = (name == "public").then(|| {
56            vec![
57                BoundAclEntry {
58                    role: Some(owner),
59                    grantor: owner,
60                    privileges: SchemaPrivileges::ALL,
61                    grant_options: SchemaPrivileges::default(),
62                },
63                BoundAclEntry {
64                    role: None,
65                    grantor: owner,
66                    privileges: SchemaPrivileges {
67                        usage: true,
68                        create: false,
69                    },
70                    grant_options: SchemaPrivileges::default(),
71                },
72            ]
73        });
74        Self {
75            name,
76            tuple: None,
77            role_owner: owner,
78            acl,
79        }
80    }
81}
82
83/// Grantable privileges carried by one schema ACL path.
84#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
85pub struct SchemaPrivileges {
86    #[serde(default)]
87    pub usage: bool,
88    #[serde(default)]
89    pub create: bool,
90}
91
92impl SchemaPrivileges {
93    pub const ALL: Self = Self {
94        usage: true,
95        create: true,
96    };
97
98    #[must_use]
99    pub const fn is_empty(self) -> bool {
100        !self.usage && !self.create
101    }
102
103    #[must_use]
104    pub const fn intersects(self, other: Self) -> bool {
105        self.usage && other.usage || self.create && other.create
106    }
107
108    pub fn insert(&mut self, other: Self) {
109        self.usage |= other.usage;
110        self.create |= other.create;
111    }
112
113    pub fn remove(&mut self, other: Self) {
114        self.usage &= !other.usage;
115        self.create &= !other.create;
116    }
117}
118
119/// One explicit schema ACL path. `None` on [`SchemaRow::acl`] retains the owner-only default privileges of an ordinary newly created schema.
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121pub struct SchemaAclEntry {
122    pub role: crate::catalog_acl::AclGrantee,
123    /// Legacy persisted entries without an explicit grantor originate from the schema owner.
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub grantor: Option<String>,
126    #[serde(default)]
127    pub privileges: SchemaPrivileges,
128    #[serde(default)]
129    pub grant_options: SchemaPrivileges,
130}
131
132/// Durable schema ownership and ACL metadata.
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134pub struct SchemaRow {
135    pub name: String,
136    /// SQL role that owns the schema. Catalogs written before schema security belonged to the bootstrap role.
137    #[serde(default = "default_schema_role_owner")]
138    pub role_owner: String,
139    /// Explicit ACL paths. `None` represents the owner-only default for an ordinary schema.
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub acl: Option<Vec<SchemaAclEntry>>,
142}
143
144impl SchemaRow {
145    #[must_use]
146    pub fn legacy(name: impl Into<String>) -> Self {
147        let name = name.into();
148        let acl = (name == "public").then(|| {
149            vec![
150                SchemaAclEntry {
151                    role: "uqa".into(),
152                    grantor: Some("uqa".into()),
153                    privileges: SchemaPrivileges::ALL,
154                    grant_options: SchemaPrivileges::default(),
155                },
156                SchemaAclEntry {
157                    role: crate::catalog_acl::AclGrantee::Public,
158                    grantor: Some("uqa".into()),
159                    privileges: SchemaPrivileges {
160                        usage: true,
161                        create: false,
162                    },
163                    grant_options: SchemaPrivileges::default(),
164                },
165            ]
166        });
167        Self {
168            name,
169            role_owner: default_schema_role_owner(),
170            acl,
171        }
172    }
173}
174
175fn default_schema_role_owner() -> String {
176    "uqa".into()
177}