Skip to main content

uqa_sql/schema/
type_objects.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Targets of `ALTER TYPE | DOMAIN` and `GRANT | REVOKE ... ON TYPE | DOMAIN`: the user-defined type a name resolves to, and `PostgreSQL`'s diagnostics for the types a command cannot change. Each command applies the checks in its own `PostgreSQL` order.
8
9use crate::ast::{ObjectAclEntry, TypeObjectKind};
10use crate::catalog::{
11    composite_type::StoredComposite, domain::StoredDomain, enum_type::StoredEnum,
12};
13use crate::schema::domains::removal::{RowTypeRelation, TypeObjectBinding};
14use crate::SQLError;
15use uqa_core::{catalog_role::RoleIdentity, RelationIdentity};
16
17/// A user-defined type whose catalog definition a lifecycle command changes. Its generated array type follows it.
18#[derive(Debug, Clone)]
19pub enum TypeObject {
20    Enum(StoredEnum),
21    Domain(Box<StoredDomain>),
22    /// A standalone composite type, whose relation follows it.
23    Composite(Box<StoredComposite>),
24}
25
26impl TypeObject {
27    #[must_use]
28    pub fn oid(&self) -> u32 {
29        match self {
30            Self::Enum(definition) => definition.oid,
31            Self::Domain(domain) => domain.oid,
32            Self::Composite(definition) => definition.oid,
33        }
34    }
35
36    #[must_use]
37    pub fn identity(&self) -> &RelationIdentity {
38        match self {
39            Self::Enum(definition) => &definition.identity,
40            Self::Domain(domain) => &domain.identity,
41            Self::Composite(definition) => &definition.identity,
42        }
43    }
44
45    #[must_use]
46    pub fn owner(&self) -> RoleIdentity {
47        match self {
48            Self::Enum(definition) => definition.owner,
49            Self::Domain(domain) => domain.owner,
50            Self::Composite(definition) => definition.owner,
51        }
52    }
53
54    #[must_use]
55    pub fn array_name(&self) -> String {
56        match self {
57            Self::Enum(definition) => definition.array_name.clone(),
58            Self::Domain(domain) => domain.array_type_name(),
59            Self::Composite(definition) => definition.array_name.clone(),
60        }
61    }
62
63    #[must_use]
64    pub fn usage_acl(&self) -> Option<&[ObjectAclEntry]> {
65        match self {
66            Self::Enum(definition) => definition.usage_acl.as_deref(),
67            Self::Domain(domain) => domain.usage_acl.as_deref(),
68            Self::Composite(definition) => definition.usage_acl.as_deref(),
69        }
70    }
71}
72
73/// What a type name resolved to before command-specific checks.
74#[derive(Debug, Clone)]
75enum ResolvedKind {
76    User(TypeObject),
77    /// The generated array type of a user-defined type or relation row type.
78    Array {
79        element: u32,
80    },
81    RowType(RowTypeRelation),
82    /// A type that no role created: built-in types and fixed system catalog domains.
83    System,
84}
85
86#[derive(Debug, Clone)]
87pub struct ResolvedTypeObject {
88    oid: u32,
89    kind: ResolvedKind,
90}
91
92/// Resolve an `any_name` type-object target as identifiers, without interpreting SQL type aliases such as `integer`. The lookup uses quoted components while the ordinary resolver preserves namespace checks and diagnostics.
93pub fn resolve_named_type_object(
94    context: &TypeObjectBinding<'_>,
95    name: &str,
96) -> Result<ResolvedTypeObject, SQLError> {
97    let parts = crate::parse_regobject_name(name)
98        .ok_or_else(|| SQLError::Internal("type object target has no valid name".into()))?;
99    let lookup = parts
100        .iter()
101        .map(|part| format!("\"{}\"", part.replace('"', "\"\"")))
102        .collect::<Vec<_>>()
103        .join(".");
104    resolve_type_object(context, &lookup)
105}
106
107/// Resolve a type name as `typenameTypeId` does: a missing schema is 3F000 and a schema without USAGE is 42501 before the type lookup reports 42704.
108pub fn resolve_type_object(
109    context: &TypeObjectBinding<'_>,
110    name: &str,
111) -> Result<ResolvedTypeObject, SQLError> {
112    let parsed = crate::parse_regtype_name(name)?
113        .ok_or_else(|| SQLError::Internal("ALTER TYPE has no type name".into()))?;
114    let label = format!(
115        "{}{}",
116        parsed.names.join("."),
117        "[]".repeat(parsed.array_dimensions)
118    );
119    if let [schema, _] = parsed.names.as_slice() {
120        if context.catalog.schema_security(schema).is_none() {
121            return Err(SQLError::Routine {
122                sqlstate: "3F000".into(),
123                message: format!("schema \"{schema}\" does not exist"),
124            });
125        }
126        if !context
127            .authority
128            .schema_usage(schema, &context.session.current_role())
129        {
130            return Err(SQLError::Routine {
131                sqlstate: "42501".into(),
132                message: format!("permission denied for schema {schema}"),
133            });
134        }
135    }
136    let oid = context
137        .catalog
138        .resolve_drop_type_oid(name)?
139        .ok_or_else(|| SQLError::Routine {
140            sqlstate: "42704".into(),
141            message: format!("type \"{label}\" does not exist"),
142        })?;
143    let oid = u32::try_from(oid)
144        .map_err(|_| SQLError::Internal(format!("type OID {oid} is out of range")))?;
145    Ok(ResolvedTypeObject {
146        oid,
147        kind: classify(context, oid),
148    })
149}
150
151fn classify(context: &TypeObjectBinding<'_>, oid: u32) -> ResolvedKind {
152    if let Some(domain) = context.catalog.domain_by_oid(oid) {
153        return ResolvedKind::User(TypeObject::Domain(Box::new(domain)));
154    }
155    if let Some(definition) = context.catalog.enum_by_type_oid(oid) {
156        if definition.array_oid == oid {
157            return ResolvedKind::Array {
158                element: definition.oid,
159            };
160        }
161        return ResolvedKind::User(TypeObject::Enum(definition));
162    }
163    if let Some(definition) = context.catalog.composite_by_type_oid(oid) {
164        if definition.array_oid == oid {
165            return ResolvedKind::Array {
166                element: definition.oid,
167            };
168        }
169        return ResolvedKind::User(TypeObject::Composite(Box::new(definition)));
170    }
171    if let Some(element) = context.catalog.user_array_element(oid) {
172        return ResolvedKind::Array { element };
173    }
174    if let Some(relation) = context.catalog.row_type_relation(oid) {
175        return ResolvedKind::RowType(relation);
176    }
177    ResolvedKind::System
178}
179
180impl ResolvedTypeObject {
181    #[must_use]
182    pub fn oid(&self) -> u32 {
183        self.oid
184    }
185
186    fn owner(&self, context: &TypeObjectBinding<'_>) -> RoleIdentity {
187        match &self.kind {
188            ResolvedKind::User(object) => object.owner(),
189            ResolvedKind::Array { element } => context
190                .catalog
191                .domain_by_oid(*element)
192                .map(|domain| domain.owner)
193                .or_else(|| {
194                    context
195                        .catalog
196                        .enum_by_type_oid(*element)
197                        .map(|definition| definition.owner)
198                })
199                .or_else(|| {
200                    context
201                        .catalog
202                        .composite_by_type_oid(*element)
203                        .map(|definition| definition.owner)
204                })
205                .unwrap_or(RoleIdentity::BOOTSTRAP),
206            ResolvedKind::RowType(relation) => relation.owner,
207            ResolvedKind::System => RoleIdentity::BOOTSTRAP,
208        }
209    }
210
211    /// `ALTER DOMAIN` and `GRANT ... ON DOMAIN` name only domains.
212    pub fn require_domain_keyword(
213        &self,
214        context: &TypeObjectBinding<'_>,
215        kind: TypeObjectKind,
216    ) -> Result<(), SQLError> {
217        if kind == TypeObjectKind::Domain
218            && !matches!(self.kind, ResolvedKind::User(TypeObject::Domain(_)))
219        {
220            return Err(SQLError::Routine {
221                sqlstate: "42809".into(),
222                message: format!("{} is not a domain", format_type(context, self.oid)?),
223            });
224        }
225        Ok(())
226    }
227
228    /// `object_ownercheck` on the type: the current role must have the owner's privileges. An array type is owned by its element's owner.
229    pub fn require_owner(&self, context: &TypeObjectBinding<'_>) -> Result<(), SQLError> {
230        if context
231            .authority
232            .current_user_has_role_privileges(&self.owner(context))
233        {
234            return Ok(());
235        }
236        Err(SQLError::Routine {
237            sqlstate: "42501".into(),
238            message: format!(
239                "must be owner of type {}",
240                format_type(context, self.array_element().unwrap_or(self.oid))?
241            ),
242        })
243    }
244
245    /// The relation of a table row type, which `SET SCHEMA` reports after its duplicate-name check.
246    #[must_use]
247    pub fn row_type_identity(&self) -> Option<RelationIdentity> {
248        match &self.kind {
249            ResolvedKind::RowType(relation) => Some(RelationIdentity::new(
250                &relation.schema,
251                &relation.local_name,
252            )),
253            _ => None,
254        }
255    }
256
257    fn array_element(&self) -> Option<u32> {
258        match self.kind {
259            ResolvedKind::Array { element } => Some(element),
260            _ => None,
261        }
262    }
263
264    /// A table's row type is altered through `ALTER TABLE`.
265    pub fn reject_row_type(&self, context: &TypeObjectBinding<'_>) -> Result<(), SQLError> {
266        if let ResolvedKind::RowType(_) = self.kind {
267            return Err(SQLError::Diagnostic {
268                sqlstate: "42809".into(),
269                message: format!("{} is a table's row type", format_type(context, self.oid)?),
270                detail: None,
271                hint: Some("Use ALTER TABLE instead.".into()),
272            });
273        }
274        Ok(())
275    }
276
277    /// A generated array type follows its element type.
278    pub fn reject_array(&self, context: &TypeObjectBinding<'_>) -> Result<(), SQLError> {
279        if let Some(element) = self.array_element() {
280            return Err(SQLError::Diagnostic {
281                sqlstate: "42809".into(),
282                message: format!(
283                    "cannot alter array type {}",
284                    format_type(context, self.oid)?
285                ),
286                detail: None,
287                hint: Some(format!(
288                    "You can alter type {}, which will alter the array type as well.",
289                    format_type(context, element)?
290                )),
291            });
292        }
293        Ok(())
294    }
295
296    /// `ExecGrant_Type_check`: array types have no privileges of their own, and `ON DOMAIN` names only domains.
297    pub fn require_grant_target(
298        &self,
299        context: &TypeObjectBinding<'_>,
300        kind: TypeObjectKind,
301    ) -> Result<(), SQLError> {
302        if self.array_element().is_some() {
303            return Err(SQLError::Diagnostic {
304                sqlstate: "0LP01".into(),
305                message: "cannot set privileges of array types".into(),
306                detail: None,
307                hint: Some("Set the privileges of the element type instead.".into()),
308            });
309        }
310        if kind == TypeObjectKind::Domain
311            && !matches!(self.kind, ResolvedKind::User(TypeObject::Domain(_)))
312        {
313            return Err(SQLError::Routine {
314                sqlstate: "42809".into(),
315                message: format!("\"{}\" is not a domain", format_type(context, self.oid)?),
316            });
317        }
318        Ok(())
319    }
320
321    /// The user-defined type to change. Built-in types, system catalog domains and relation row types have fixed definitions here.
322    pub fn into_type_object(self, context: &TypeObjectBinding<'_>) -> Result<TypeObject, SQLError> {
323        match self.kind {
324            ResolvedKind::User(object) => Ok(object),
325            ResolvedKind::Array { .. } | ResolvedKind::RowType(_) | ResolvedKind::System => {
326                Err(SQLError::Routine {
327                    sqlstate: "0A000".into(),
328                    message: format!(
329                        "type {} has a fixed system definition",
330                        format_type(context, self.oid)?
331                    ),
332                })
333            }
334        }
335    }
336}
337
338fn format_type(context: &TypeObjectBinding<'_>, oid: u32) -> Result<String, SQLError> {
339    context
340        .catalog
341        .format_drop_type(i64::from(oid))
342        .map_err(SQLError::Internal)?
343        .ok_or_else(|| SQLError::Internal(format!("type {oid} disappeared")))
344}
345
346#[cfg(test)]
347mod tests;