1use crate::catalog::roles::RoleReference;
10use crate::{
11 catalog::{domain::DomainCatalog, roles::RoleReferenceNames, security::BoundSchemaSecurity},
12 SQLError,
13};
14
15pub trait TypeObjectCatalog: DomainCatalog {
16 fn schema_security(&self, name: &str) -> Option<BoundSchemaSecurity>;
17 fn resolve_drop_type_oid(&self, name: &str) -> Result<Option<i64>, SQLError>;
18 fn format_drop_type(&self, oid: i64) -> Result<Option<String>, String>;
19 fn enum_by_type_oid(&self, oid: u32) -> Option<crate::catalog::enum_type::StoredEnum>;
21 fn composite_by_type_oid(
23 &self,
24 oid: u32,
25 ) -> Option<crate::catalog::composite_type::StoredComposite>;
26 fn user_array_element(&self, oid: u32) -> Option<u32>;
28 fn row_type_relation(&self, oid: u32) -> Option<RowTypeRelation>;
30}
31
32#[derive(Debug, Clone)]
34pub struct RowTypeRelation {
35 pub kind: &'static str,
36 pub name: String,
38 pub local_name: String,
40 pub owner: uqa_core::catalog_role::RoleIdentity,
41 pub schema: String,
42}
43
44#[derive(Clone, Copy, PartialEq, Eq)]
45enum TypeDropKind {
46 Domain,
47 Type,
48}
49pub trait TypeObjectAuthority {
50 fn schema_usage(&self, schema: &str, role: &RoleReference) -> bool;
51 fn current_user_has_role_privileges(
52 &self,
53 role: &dyn crate::catalog::roles::identity::RoleSubject,
54 ) -> bool;
55}
56pub struct TypeObjectBinding<'a> {
57 pub catalog: &'a dyn TypeObjectCatalog,
58 pub authority: &'a dyn TypeObjectAuthority,
59 pub session: &'a dyn RoleReferenceNames,
60}
61pub enum BoundTypeDrop {
62 Target(u32),
63 Skipped(String),
64}
65
66pub fn resolve_drop_domain(
67 context: &TypeObjectBinding<'_>,
68 name: &str,
69 if_exists: bool,
70) -> Result<BoundTypeDrop, SQLError> {
71 resolve_type_drop(context, name, if_exists, TypeDropKind::Domain)
72}
73
74pub fn resolve_drop_type(
76 context: &TypeObjectBinding<'_>,
77 name: &str,
78 if_exists: bool,
79) -> Result<BoundTypeDrop, SQLError> {
80 resolve_type_drop(context, name, if_exists, TypeDropKind::Type)
81}
82
83fn resolve_type_drop(
84 context: &TypeObjectBinding<'_>,
85 name: &str,
86 if_exists: bool,
87 kind: TypeDropKind,
88) -> Result<BoundTypeDrop, SQLError> {
89 let parsed = crate::parse_regtype_name(name)?
90 .ok_or_else(|| SQLError::Internal("DROP has no type name".into()))?;
91 let label = format!(
92 "{}{}",
93 parsed.names.join("."),
94 "[]".repeat(parsed.array_dimensions)
95 );
96 if let [schema, _] = parsed.names.as_slice() {
97 if context.catalog.schema_security(schema).is_none() {
98 if if_exists {
99 return Ok(BoundTypeDrop::Skipped(format!(
100 "schema \"{schema}\" does not exist, skipping"
101 )));
102 }
103 return Err(SQLError::Routine {
104 sqlstate: "3F000".into(),
105 message: format!("schema \"{schema}\" does not exist"),
106 });
107 }
108 if !context
109 .authority
110 .schema_usage(schema, &context.session.current_role())
111 {
112 return Err(SQLError::Routine {
113 sqlstate: "42501".into(),
114 message: format!("permission denied for schema {schema}"),
115 });
116 }
117 }
118 let oid = context.catalog.resolve_drop_type_oid(name)?;
119 let Some(oid) = oid else {
120 if if_exists {
121 return Ok(BoundTypeDrop::Skipped(format!(
122 "type \"{label}\" does not exist, skipping"
123 )));
124 }
125 return Err(SQLError::Routine {
126 sqlstate: "42704".into(),
127 message: format!("type \"{label}\" does not exist"),
128 });
129 };
130 let type_oid = u32::try_from(oid)
131 .map_err(|_| SQLError::Internal(format!("DROP type OID {oid} is out of range")))?;
132 let target = drop_target(context, type_oid, &label, kind)?;
133 let owns_schema = target.schema.as_ref().is_some_and(|schema| {
134 context
135 .catalog
136 .schema_security(schema)
137 .is_some_and(|security| {
138 context
139 .authority
140 .current_user_has_role_privileges(&security.role_owner)
141 })
142 });
143 if !owns_schema
144 && !context
145 .authority
146 .current_user_has_role_privileges(&target.owner)
147 {
148 return Err(SQLError::Routine {
149 sqlstate: "42501".into(),
150 message: format!("must be owner of type {}", format_type(context, oid)?),
151 });
152 }
153 match target.required_by {
154 None => Ok(BoundTypeDrop::Target(type_oid)),
155 Some(Requirement::System) => Err(SQLError::Routine {
156 sqlstate: "2BP01".into(),
157 message: format!(
158 "cannot drop type {} because it is required by the database system",
159 format_type(context, oid)?
160 ),
161 }),
162 Some(Requirement::Object(object)) => Err(SQLError::Diagnostic {
163 sqlstate: "2BP01".into(),
164 message: format!(
165 "cannot drop type {} because {object} requires it",
166 format_type(context, oid)?
167 ),
168 detail: None,
169 hint: Some(format!("You can drop {object} instead.")),
170 }),
171 }
172}
173
174enum Requirement {
175 System,
176 Object(String),
177}
178
179struct DropTarget {
180 schema: Option<String>,
181 owner: uqa_core::catalog_role::RoleIdentity,
182 required_by: Option<Requirement>,
183}
184
185fn drop_target(
186 context: &TypeObjectBinding<'_>,
187 oid: u32,
188 label: &str,
189 kind: TypeDropKind,
190) -> Result<DropTarget, SQLError> {
191 if let Some(domain) = context.catalog.domain_by_oid(oid) {
192 return Ok(DropTarget {
193 schema: Some(domain.identity.schema),
194 owner: domain.owner,
195 required_by: None,
196 });
197 }
198 if kind == TypeDropKind::Domain {
199 return Err(SQLError::Routine {
200 sqlstate: "42809".into(),
201 message: format!("\"{label}\" is not a domain"),
202 });
203 }
204 if let Some(definition) = context.catalog.enum_by_type_oid(oid) {
205 return Ok(DropTarget {
206 schema: Some(definition.identity.schema.clone()),
207 owner: definition.owner,
208 required_by: (definition.array_oid == oid)
209 .then(|| {
210 format_type(context, i64::from(definition.oid))
211 .map(|element| Requirement::Object(format!("type {element}")))
212 })
213 .transpose()?,
214 });
215 }
216 if let Some(definition) = context.catalog.composite_by_type_oid(oid) {
217 return Ok(DropTarget {
218 schema: Some(definition.identity.schema.clone()),
219 owner: definition.owner,
220 required_by: (definition.array_oid == oid)
221 .then(|| {
222 format_type(context, i64::from(definition.oid))
223 .map(|element| Requirement::Object(format!("type {element}")))
224 })
225 .transpose()?,
226 });
227 }
228 if let Some(element) = context.catalog.user_array_element(oid) {
229 let domain = context
230 .catalog
231 .domain_by_oid(element)
232 .ok_or_else(|| SQLError::Internal(format!("array type {oid} has no element type")))?;
233 return Ok(DropTarget {
234 schema: Some(domain.identity.schema),
235 owner: domain.owner,
236 required_by: Some(Requirement::Object(format!(
237 "type {}",
238 format_type(context, i64::from(element))?
239 ))),
240 });
241 }
242 if let Some(relation) = context.catalog.row_type_relation(oid) {
243 return Ok(DropTarget {
244 schema: Some(relation.schema),
245 owner: relation.owner,
246 required_by: Some(Requirement::Object(format!(
247 "{} {}",
248 relation.kind, relation.name
249 ))),
250 });
251 }
252 Ok(DropTarget {
253 schema: None,
254 owner: uqa_core::catalog_role::RoleIdentity::BOOTSTRAP,
255 required_by: Some(Requirement::System),
256 })
257}
258
259fn format_type(context: &TypeObjectBinding<'_>, oid: i64) -> Result<String, SQLError> {
260 context
261 .catalog
262 .format_drop_type(oid)
263 .map_err(SQLError::Internal)?
264 .ok_or_else(|| SQLError::Internal("DROP type target disappeared".into()))
265}
266
267pub fn resolve_alter_enum(
269 context: &TypeObjectBinding<'_>,
270 name: &str,
271) -> Result<crate::catalog::enum_type::StoredEnum, SQLError> {
272 let parsed = crate::parse_regtype_name(name)?
273 .ok_or_else(|| SQLError::Internal("ALTER TYPE has no type name".into()))?;
274 let label = format!(
275 "{}{}",
276 parsed.names.join("."),
277 "[]".repeat(parsed.array_dimensions)
278 );
279 let oid = context
280 .catalog
281 .resolve_drop_type_oid(name)?
282 .ok_or_else(|| SQLError::Routine {
283 sqlstate: "42704".into(),
284 message: format!("type \"{label}\" does not exist"),
285 })?;
286 let definition = u32::try_from(oid)
287 .ok()
288 .and_then(|oid| context.catalog.enum_by_type_oid(oid))
289 .filter(|definition| i64::from(definition.oid) == oid)
290 .ok_or_else(|| {
291 format_type(context, oid).map_or_else(
292 |error| error,
293 |name| SQLError::Routine {
294 sqlstate: "42809".into(),
295 message: format!("{name} is not an enum"),
296 },
297 )
298 })?;
299 if !context
301 .authority
302 .current_user_has_role_privileges(&definition.owner)
303 {
304 return Err(SQLError::Routine {
305 sqlstate: "42501".into(),
306 message: format!("must be owner of type {}", format_type(context, oid)?),
307 });
308 }
309 Ok(definition)
310}