1use super::EventAnalysisContext;
8use crate::{
9 ast::{
10 ColumnDef, ColumnType, CreateTrigger, Expr, FunctionReturns, TriggerEvent, TriggerTiming,
11 },
12 catalog::{
13 events::validation::{
14 is_boolean_type, validate_trigger_condition_references,
15 validate_trigger_transition_relation, TriggerConditionTypeResolver,
16 },
17 resolution::{RelationLookupMode, RelationResolution},
18 security::table::TableAclPrivilege,
19 },
20 plpgsql::bind_expr,
21 routines::{routine_signature_types, CompiledFunctionBody, SQLUserFunction},
22 type_resolution::canonical_routine_type_name,
23 SQLError,
24};
25use std::sync::Arc;
26use uqa_core::{RelationIdentity, Value};
27
28impl EventAnalysisContext<'_> {
29 pub fn resolve_trigger_table(&self, name: &str) -> Result<RelationIdentity, SQLError> {
30 let RelationResolution::Found(canonical, kind) =
31 self.relations.resolve_bound_relation_kind(name)?
32 else {
33 return Err(SQLError::UnknownTable(name.to_string()));
34 };
35 if !matches!(
36 kind,
37 "table" | "view" | "materialized view" | "foreign table"
38 ) {
39 return Err(SQLError::UnknownTable(name.to_string()));
40 }
41 RelationIdentity::from_legacy_name(&canonical).map_err(|error| {
42 SQLError::Internal(format!(
43 "decode bound trigger relation `{canonical}`: {error}"
44 ))
45 })
46 }
47
48 fn trigger_relation_columns(
49 &self,
50 relation: &RelationIdentity,
51 kind: &str,
52 ) -> Result<Vec<ColumnDef>, SQLError> {
53 if kind == "table" {
54 return self
55 .returning
56 .catalog
57 .try_describe_table_row_type(&relation.qualified_name())
58 .map_err(|error| SQLError::Internal(format!("read trigger columns: {error}")))?
59 .ok_or_else(|| SQLError::UnknownTable(relation.qualified_name()));
60 }
61 if kind == "foreign table" {
62 let columns = self
63 .catalog
64 .foreign_columns(relation)
65 .ok_or_else(|| SQLError::UnknownTable(relation.qualified_name()))?;
66 return Ok(columns
67 .into_iter()
68 .map(|column| trigger_column(column.name, column.ty))
69 .collect());
70 }
71 let view = self
72 .catalog
73 .restored_catalog_view_definition(&relation.qualified_name())?
74 .ok_or_else(|| SQLError::UnknownTable(relation.qualified_name()))?;
75 let schema = self.catalog.stored_view_schema(&view)?;
76 Ok(schema
77 .columns()
78 .iter()
79 .enumerate()
80 .map(|(position, name)| {
81 trigger_column(
82 schema.public_name(position).unwrap_or(name).to_string(),
83 schema
84 .column_type(position)
85 .cloned()
86 .unwrap_or(ColumnType::Text),
87 )
88 })
89 .collect())
90 }
91
92 pub fn trigger_relation_from_resolution(
93 name: &str,
94 resolution: RelationResolution,
95 ) -> Result<(RelationIdentity, &'static str), SQLError> {
96 match resolution {
97 RelationResolution::Found(canonical, "foreign table") => {
98 let relation = RelationIdentity::from_legacy_name(&canonical).map_err(|error| {
99 SQLError::Internal(format!(
100 "decode resolved trigger relation `{canonical}`: {error}"
101 ))
102 })?;
103 Ok((relation, "foreign table"))
104 }
105 resolution => Self::event_relation_from_resolution(name, resolution),
106 }
107 }
108
109 pub fn resolve_trigger_relation_kind(
110 &self,
111 name: &str,
112 lookup_mode: RelationLookupMode,
113 ) -> Result<(RelationIdentity, &'static str), SQLError> {
114 let resolution = match lookup_mode {
115 RelationLookupMode::Dynamic => self.relations.resolve_visible_relation_kind(name)?,
116 RelationLookupMode::Bound => self.relations.resolve_bound_relation_kind(name)?,
117 };
118 Self::trigger_relation_from_resolution(name, resolution)
119 }
120
121 fn resolve_trigger_function_candidate(
122 &self,
123 name: &str,
124 lookup_mode: RelationLookupMode,
125 ) -> Result<Arc<SQLUserFunction>, SQLError> {
126 let candidates = match lookup_mode {
127 RelationLookupMode::Dynamic => self.routines.lookup_visible_sql_functions(name)?,
128 RelationLookupMode::Bound => self.routines.lookup_bound_sql_functions(name),
129 }
130 .unwrap_or_default()
131 .into_iter()
132 .filter(|function| {
133 !function.def.is_procedure && routine_signature_types(&function.def).is_empty()
134 })
135 .collect::<Vec<_>>();
136 let function = match candidates.as_slice() {
137 [function] => function.clone(),
138 [] => {
139 return Err(SQLError::Routine {
140 sqlstate: "42883".into(),
141 message: format!("function {name}() does not exist"),
142 })
143 }
144 _ => {
145 return Err(SQLError::Routine {
146 sqlstate: "42725".into(),
147 message: format!("function name \"{name}\" is not unique"),
148 })
149 }
150 };
151 Ok(function)
152 }
153
154 fn validate_trigger_function(function: &SQLUserFunction) -> Result<(), SQLError> {
155 let returns_trigger = matches!(
156 &function.def.returns,
157 FunctionReturns::Scalar { type_name }
158 if canonical_routine_type_name(type_name) == "trigger"
159 );
160 if !returns_trigger {
161 return Err(SQLError::Routine {
162 sqlstate: "42P17".into(),
163 message: format!("function {} must return type trigger", function.def.name),
164 });
165 }
166 if !matches!(function.compiled, CompiledFunctionBody::PLpgSQL(_)) {
167 return Err(SQLError::Routine {
168 sqlstate: "0A000".into(),
169 message: "only LANGUAGE plpgsql trigger functions are executable".into(),
170 });
171 }
172 Ok(())
173 }
174
175 pub fn resolve_trigger_function(
176 &self,
177 name: &str,
178 lookup_mode: RelationLookupMode,
179 ) -> Result<Arc<SQLUserFunction>, SQLError> {
180 let function = self.resolve_trigger_function_candidate(name, lookup_mode)?;
181 Self::validate_trigger_function(&function)?;
182 Ok(function)
183 }
184
185 pub fn resolve_bound_trigger_function(
186 &self,
187 name: &str,
188 object_id: Option<[u8; 16]>,
189 ) -> Result<Arc<SQLUserFunction>, SQLError> {
190 let Some(object_id) = object_id else {
191 return self.resolve_trigger_function(name, RelationLookupMode::Bound);
192 };
193 let binding = crate::ast::FunctionBinding {
194 object_id: Some(object_id),
195 name: name.to_string(),
196 argument_types: Vec::new(),
197 builtin: false,
198 dispatch: None,
199 invocation: None,
200 resolution_error: None,
201 };
202 let candidates = self
203 .routines
204 .lookup_bound_sql_functions_by_binding(&binding)
205 .unwrap_or_default()
206 .into_iter()
207 .filter(|function| {
208 !function.def.is_procedure && routine_signature_types(&function.def).is_empty()
209 })
210 .collect::<Vec<_>>();
211 let function = match candidates.as_slice() {
212 [function] => function.clone(),
213 [] => {
214 return Err(SQLError::Routine {
215 sqlstate: "42883".into(),
216 message: format!("function {name}() does not exist"),
217 })
218 }
219 _ => {
220 return Err(SQLError::Internal(format!(
221 "routine object identity for trigger function `{name}` is not unique"
222 )))
223 }
224 };
225 Self::validate_trigger_function(&function)?;
226 Ok(function)
227 }
228
229 fn ensure_trigger_creation_privilege(
230 &self,
231 relation: &RelationIdentity,
232 relation_kind: &str,
233 ) -> Result<(), SQLError> {
234 let canonical = relation.qualified_name();
235 match relation_kind {
236 "table" => {
237 let current_user = self.authority.current_user_name();
238 self.privileges.ensure_table_privilege_for(
239 &canonical,
240 ¤t_user,
241 TableAclPrivilege::Trigger,
242 )
243 }
244 "view" => {
245 let view = self
246 .catalog
247 .restored_catalog_view_definition(&canonical)?
248 .ok_or_else(|| {
249 SQLError::Internal(format!(
250 "resolved trigger view `{canonical}` has no catalog definition"
251 ))
252 })?;
253 self.privileges.ensure_view_privilege_for(
254 &canonical,
255 &view,
256 &self.authority.current_user_name(),
257 TableAclPrivilege::Trigger,
258 )
259 }
260 "foreign table" => self
261 .foreign_privileges
262 .ensure_foreign_table_privilege(&canonical, TableAclPrivilege::Trigger),
263 _ => Ok(()),
264 }
265 }
266
267 fn validate_trigger_relation_kind(
268 definition: &CreateTrigger,
269 relation: &RelationIdentity,
270 relation_kind: &str,
271 ) -> Result<(), SQLError> {
272 match relation_kind {
273 "table" if definition.timing == TriggerTiming::InsteadOf => Err(SQLError::Routine {
274 sqlstate: "42809".into(),
275 message: format!("\"{}\" is a table", relation.name),
276 }),
277 "view" if definition.timing == TriggerTiming::InsteadOf => {
278 if definition.events.contains(&TriggerEvent::Truncate)
279 || !definition.transition_relations.is_empty()
280 {
281 return Err(SQLError::Routine {
282 sqlstate: "42809".into(),
283 message: format!("\"{}\" is a view", relation.name),
284 });
285 }
286 if !definition.row {
287 return Err(SQLError::Routine {
288 sqlstate: "0A000".into(),
289 message: "INSTEAD OF triggers must be FOR EACH ROW".into(),
290 });
291 }
292 if definition.when.is_some() {
293 return Err(SQLError::Routine {
294 sqlstate: "0A000".into(),
295 message: "INSTEAD OF triggers cannot have WHEN conditions".into(),
296 });
297 }
298 if !definition.update_columns.is_empty() {
299 return Err(SQLError::Routine {
300 sqlstate: "0A000".into(),
301 message: "INSTEAD OF triggers cannot have column lists".into(),
302 });
303 }
304 Ok(())
305 }
306 "view"
307 if definition.events.contains(&TriggerEvent::Truncate)
308 || !definition.transition_relations.is_empty()
309 || definition.row =>
310 {
311 Err(SQLError::Routine {
312 sqlstate: "42809".into(),
313 message: format!("\"{}\" is a view", relation.name),
314 })
315 }
316 "materialized view" => Err(SQLError::Routine {
317 sqlstate: "42809".into(),
318 message: format!("relation \"{}\" cannot have triggers", relation.name),
319 }),
320 "foreign table"
321 if definition.constraint || definition.timing == TriggerTiming::InsteadOf =>
322 {
323 Err(SQLError::Routine {
324 sqlstate: "42809".into(),
325 message: format!("\"{}\" is a foreign table", relation.name),
326 })
327 }
328 "view" | "foreign table" => Ok(()),
329 kind if kind != "table" => Err(SQLError::Routine {
330 sqlstate: "42809".into(),
331 message: format!("relation \"{}\" cannot have triggers", relation.name),
332 }),
333 _ => Ok(()),
334 }
335 }
336
337 pub fn validate_trigger_definition(
338 &self,
339 definition: &mut CreateTrigger,
340 lookup_mode: RelationLookupMode,
341 ) -> Result<(RelationIdentity, bool), SQLError> {
342 if definition.constraint && definition.or_replace {
343 return Err(SQLError::Routine {
344 sqlstate: "0A000".into(),
345 message: "CREATE OR REPLACE CONSTRAINT TRIGGER is not supported".into(),
346 });
347 }
348 if definition.constraint && (!definition.row || definition.timing != TriggerTiming::After) {
349 return Err(SQLError::Routine {
350 sqlstate: "0A000".into(),
351 message: "constraint triggers must be AFTER ROW triggers".into(),
352 });
353 }
354 if !definition.constraint
355 && (definition.deferrability.is_deferrable() || definition.referenced_table.is_some())
356 {
357 return Err(SQLError::Internal(
358 "ordinary trigger retained constraint-only metadata".into(),
359 ));
360 }
361 let (relation, relation_kind) =
362 self.resolve_trigger_relation_kind(&definition.table, lookup_mode)?;
363 definition.table = relation.qualified_name();
364 Self::validate_trigger_relation_kind(definition, &relation, relation_kind)?;
365 if lookup_mode == RelationLookupMode::Dynamic {
366 self.ensure_trigger_creation_privilege(&relation, relation_kind)?;
367 }
368 if relation_kind == "foreign table" && !definition.transition_relations.is_empty() {
369 return Err(SQLError::Routine {
370 sqlstate: "42809".into(),
371 message: format!("\"{}\" is a foreign table", relation.name),
372 });
373 }
374 if let Some(referenced_table) = definition.referenced_table.as_mut() {
375 let (referenced, referenced_kind) =
376 self.resolve_event_relation_kind(referenced_table, lookup_mode)?;
377 if referenced_kind != "table" {
378 return Err(SQLError::Routine {
379 sqlstate: "42809".into(),
380 message: format!("\"{}\" is a {referenced_kind}", referenced.name),
381 });
382 }
383 *referenced_table = referenced.qualified_name();
384 }
385 let requested_function = definition.function.clone();
386 let function = self.resolve_trigger_function_candidate(&requested_function, lookup_mode)?;
387 if lookup_mode == RelationLookupMode::Dynamic {
388 crate::routines::security::ensure_routine_execute_privilege_named(
389 self.authority,
390 &function.def,
391 &requested_function,
392 )?;
393 }
394 Self::validate_trigger_function(&function)?;
395 definition.function.clone_from(&function.def.name);
396 let columns = self.trigger_relation_columns(&relation, relation_kind)?;
397 if relation_kind == "table" {
398 self.validate_trigger_transition_relations(definition, &relation)?;
399 }
400 if definition.events.contains(&TriggerEvent::Truncate) && definition.row {
401 return Err(SQLError::Routine {
402 sqlstate: "0A000".into(),
403 message: "TRUNCATE FOR EACH ROW triggers are not supported".into(),
404 });
405 }
406 if !definition.update_columns.is_empty()
407 && !definition.events.contains(&TriggerEvent::Update)
408 {
409 return Err(SQLError::Routine {
410 sqlstate: "42601".into(),
411 message: "UPDATE OF columns may only be specified for an UPDATE trigger".into(),
412 });
413 }
414 let mut seen_update_columns = std::collections::BTreeSet::new();
415 for column in &definition.update_columns {
416 if !columns.iter().any(|definition| definition.name == *column) {
417 return Err(SQLError::UnknownColumn(format!(
418 "{}.{column}",
419 definition.table
420 )));
421 }
422 if !seen_update_columns.insert(column) {
423 return Err(SQLError::Routine {
424 sqlstate: "42701".into(),
425 message: format!("column \"{column}\" specified more than once"),
426 });
427 }
428 }
429 let mut condition_routine_bindings_changed = false;
430 if let Some(mut condition) = definition.when.take() {
431 condition_routine_bindings_changed =
432 self.validate_trigger_condition(definition, &columns, &mut condition)?;
433 definition.when = Some(condition);
434 }
435 Ok((relation, condition_routine_bindings_changed))
436 }
437
438 fn validate_trigger_transition_relations(
439 &self,
440 definition: &CreateTrigger,
441 relation: &RelationIdentity,
442 ) -> Result<(), SQLError> {
443 if definition.transition_relations.is_empty() {
444 return Ok(());
445 }
446 let hierarchy = self
447 .catalog
448 .loaded_table_hierarchy(relation)
449 .ok_or_else(|| {
450 SQLError::Internal(format!(
451 "trigger table `{}` disappeared during validation",
452 definition.table
453 ))
454 })?;
455 if definition.row && hierarchy.partition_spec.is_some() {
456 return Err(SQLError::Routine {
457 sqlstate: "0A000".into(),
458 message: format!("\"{}\" is a partitioned table", relation.name),
459 });
460 }
461 let mut old_table = None;
462 let mut new_table = None;
463 for transition in &definition.transition_relations {
464 validate_trigger_transition_relation(definition, &hierarchy, transition)?;
465 let duplicate = if transition.is_new {
466 new_table.replace(transition.name.as_str())
467 } else {
468 old_table.replace(transition.name.as_str())
469 };
470 if duplicate.is_some() {
471 return Err(SQLError::Routine {
472 sqlstate: "42P17".into(),
473 message: format!(
474 "{} TABLE cannot be specified multiple times",
475 if transition.is_new { "NEW" } else { "OLD" }
476 ),
477 });
478 }
479 }
480 if old_table.is_some() && old_table == new_table {
481 return Err(SQLError::Routine {
482 sqlstate: "42P17".into(),
483 message: "OLD TABLE name and NEW TABLE name cannot be the same".into(),
484 });
485 }
486 Ok(())
487 }
488
489 fn validate_trigger_condition(
490 &self,
491 definition: &CreateTrigger,
492 columns: &[crate::ast::ColumnDef],
493 condition: &mut Expr,
494 ) -> Result<bool, SQLError> {
495 validate_trigger_condition_references(definition, columns, condition)?;
496 let bound = bind_expr(condition, &mut TriggerConditionTypeResolver { columns })?;
497 let mut plan = crate::plan::ExpressionPlan::lower_with(bound, &|name: &str| {
498 self.routines.has_registered_aggregate_function(name)
499 });
500 let ty =
501 self.stored_routines
502 .bind_expression(&mut plan, &[], &crate::RowSchema::default())?;
503 if !ty.as_ref().is_some_and(is_boolean_type) {
504 if let Expr::Literal(value @ (Value::Str(_) | Value::FixedChar(_))) = condition {
505 *value = crate::expr::cast_value(value, "boolean")?;
506 } else if let Some(ty) = ty {
507 return Err(SQLError::TypeMismatch(format!(
508 "argument of WHEN must be type boolean, not type {}",
509 ty.sql_name()
510 )));
511 } else {
512 *condition = Expr::Cast {
513 expr: Box::new(condition.clone()),
514 ty: "boolean".into(),
515 };
516 }
517 }
518 crate::catalog::regrole_dependencies::reject_stored_regrole_constants_with(
519 self.regroles,
520 condition,
521 None,
522 )?;
523 let references =
524 crate::binding::stored_routines::collect_expression_routine_references(&plan)?;
525 crate::catalog::stored_ast::bind_stored_expression_routines(condition, &references)
526 }
527}
528
529fn trigger_column(name: String, ty: ColumnType) -> ColumnDef {
530 ColumnDef {
531 name,
532 ty,
533 object_id: None,
534 missing_value: None,
535 primary_key: false,
536 not_null: false,
537 not_null_explicit: false,
538 not_null_name: None,
539 not_null_validated: true,
540 not_null_no_inherit: false,
541 not_null_is_local: true,
542 auto_increment: None,
543 unique: false,
544 default: None,
545 generated: None,
546 check: None,
547 check_name: None,
548 check_enforced: true,
549 check_validated: true,
550 check_no_inherit: false,
551 check_is_local: true,
552 check_object_id: None,
553 references: None,
554 }
555}