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, 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 function.def.language != "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 composite_field: None,
201 resolution_error: None,
202 };
203 let candidates = self
204 .routines
205 .lookup_bound_sql_functions_by_binding(&binding)
206 .unwrap_or_default()
207 .into_iter()
208 .filter(|function| {
209 !function.def.is_procedure && routine_signature_types(&function.def).is_empty()
210 })
211 .collect::<Vec<_>>();
212 let function = match candidates.as_slice() {
213 [function] => function.clone(),
214 [] => {
215 return Err(SQLError::Routine {
216 sqlstate: "42883".into(),
217 message: format!("function {name}() does not exist"),
218 })
219 }
220 _ => {
221 return Err(SQLError::Internal(format!(
222 "routine object identity for trigger function `{name}` is not unique"
223 )))
224 }
225 };
226 Self::validate_trigger_function(&function)?;
227 Ok(function)
228 }
229
230 fn ensure_trigger_creation_privilege(
231 &self,
232 relation: &RelationIdentity,
233 relation_kind: &str,
234 ) -> Result<(), SQLError> {
235 let canonical = relation.qualified_name();
236 match relation_kind {
237 "table" => {
238 let current_user = self.authority.current_role();
239 self.privileges.ensure_table_privilege_for(
240 &canonical,
241 ¤t_user,
242 TableAclPrivilege::Trigger,
243 )
244 }
245 "view" => {
246 let view = self
247 .catalog
248 .restored_catalog_view_definition(&canonical)?
249 .ok_or_else(|| {
250 SQLError::Internal(format!(
251 "resolved trigger view `{canonical}` has no catalog definition"
252 ))
253 })?;
254 self.privileges.ensure_view_privilege_for(
255 &canonical,
256 &view,
257 &self.authority.current_role(),
258 TableAclPrivilege::Trigger,
259 )
260 }
261 "foreign table" => self
262 .foreign_privileges
263 .ensure_foreign_table_privilege(&canonical, TableAclPrivilege::Trigger),
264 _ => Ok(()),
265 }
266 }
267
268 fn validate_trigger_relation_kind(
269 definition: &CreateTrigger,
270 relation: &RelationIdentity,
271 relation_kind: &str,
272 ) -> Result<(), SQLError> {
273 match relation_kind {
274 "table" if definition.timing == TriggerTiming::InsteadOf => Err(SQLError::Routine {
275 sqlstate: "42809".into(),
276 message: format!("\"{}\" is a table", relation.name),
277 }),
278 "view" if definition.timing == TriggerTiming::InsteadOf => {
279 if definition.events.contains(&TriggerEvent::Truncate)
280 || !definition.transition_relations.is_empty()
281 {
282 return Err(SQLError::Routine {
283 sqlstate: "42809".into(),
284 message: format!("\"{}\" is a view", relation.name),
285 });
286 }
287 if !definition.row {
288 return Err(SQLError::Routine {
289 sqlstate: "0A000".into(),
290 message: "INSTEAD OF triggers must be FOR EACH ROW".into(),
291 });
292 }
293 if definition.when.is_some() {
294 return Err(SQLError::Routine {
295 sqlstate: "0A000".into(),
296 message: "INSTEAD OF triggers cannot have WHEN conditions".into(),
297 });
298 }
299 if !definition.update_columns.is_empty() {
300 return Err(SQLError::Routine {
301 sqlstate: "0A000".into(),
302 message: "INSTEAD OF triggers cannot have column lists".into(),
303 });
304 }
305 Ok(())
306 }
307 "view"
308 if definition.events.contains(&TriggerEvent::Truncate)
309 || !definition.transition_relations.is_empty()
310 || definition.row =>
311 {
312 Err(SQLError::Routine {
313 sqlstate: "42809".into(),
314 message: format!("\"{}\" is a view", relation.name),
315 })
316 }
317 "materialized view" => Err(SQLError::Routine {
318 sqlstate: "42809".into(),
319 message: format!("relation \"{}\" cannot have triggers", relation.name),
320 }),
321 "foreign table"
322 if definition.constraint || definition.timing == TriggerTiming::InsteadOf =>
323 {
324 Err(SQLError::Routine {
325 sqlstate: "42809".into(),
326 message: format!("\"{}\" is a foreign table", relation.name),
327 })
328 }
329 "view" | "foreign table" => Ok(()),
330 kind if kind != "table" => Err(SQLError::Routine {
331 sqlstate: "42809".into(),
332 message: format!("relation \"{}\" cannot have triggers", relation.name),
333 }),
334 _ => Ok(()),
335 }
336 }
337
338 pub fn validate_trigger_definition(
339 &self,
340 definition: &mut CreateTrigger,
341 lookup_mode: RelationLookupMode,
342 ) -> Result<(RelationIdentity, bool), SQLError> {
343 if definition.constraint && definition.or_replace {
344 return Err(SQLError::Routine {
345 sqlstate: "0A000".into(),
346 message: "CREATE OR REPLACE CONSTRAINT TRIGGER is not supported".into(),
347 });
348 }
349 if definition.constraint && (!definition.row || definition.timing != TriggerTiming::After) {
350 return Err(SQLError::Routine {
351 sqlstate: "0A000".into(),
352 message: "constraint triggers must be AFTER ROW triggers".into(),
353 });
354 }
355 if !definition.constraint
356 && (definition.deferrability.is_deferrable() || definition.referenced_table.is_some())
357 {
358 return Err(SQLError::Internal(
359 "ordinary trigger retained constraint-only metadata".into(),
360 ));
361 }
362 let (relation, relation_kind) =
363 self.resolve_trigger_relation_kind(&definition.table, lookup_mode)?;
364 definition.table = relation.qualified_name();
365 Self::validate_trigger_relation_kind(definition, &relation, relation_kind)?;
366 if lookup_mode == RelationLookupMode::Dynamic {
367 self.ensure_trigger_creation_privilege(&relation, relation_kind)?;
368 }
369 if relation_kind == "foreign table" && !definition.transition_relations.is_empty() {
370 return Err(SQLError::Routine {
371 sqlstate: "42809".into(),
372 message: format!("\"{}\" is a foreign table", relation.name),
373 });
374 }
375 if let Some(referenced_table) = definition.referenced_table.as_mut() {
376 let (referenced, referenced_kind) =
377 self.resolve_event_relation_kind(referenced_table, lookup_mode)?;
378 if referenced_kind != "table" {
379 return Err(SQLError::Routine {
380 sqlstate: "42809".into(),
381 message: format!("\"{}\" is a {referenced_kind}", referenced.name),
382 });
383 }
384 *referenced_table = referenced.qualified_name();
385 }
386 let requested_function = definition.function.clone();
387 let function = self.resolve_trigger_function_candidate(&requested_function, lookup_mode)?;
388 if lookup_mode == RelationLookupMode::Dynamic {
389 crate::routines::security::ensure_routine_execute_privilege_named(
390 self.authority,
391 &function.def,
392 &requested_function,
393 )?;
394 }
395 Self::validate_trigger_function(&function)?;
396 definition.function.clone_from(&function.def.name);
397 let columns = self.trigger_relation_columns(&relation, relation_kind)?;
398 if relation_kind == "table" {
399 self.validate_trigger_transition_relations(definition, &relation)?;
400 }
401 if definition.events.contains(&TriggerEvent::Truncate) && definition.row {
402 return Err(SQLError::Routine {
403 sqlstate: "0A000".into(),
404 message: "TRUNCATE FOR EACH ROW triggers are not supported".into(),
405 });
406 }
407 if !definition.update_columns.is_empty()
408 && !definition.events.contains(&TriggerEvent::Update)
409 {
410 return Err(SQLError::Routine {
411 sqlstate: "42601".into(),
412 message: "UPDATE OF columns may only be specified for an UPDATE trigger".into(),
413 });
414 }
415 let mut seen_update_columns = std::collections::BTreeSet::new();
416 for column in &definition.update_columns {
417 if !columns.iter().any(|definition| definition.name == *column) {
418 return Err(SQLError::UnknownColumn(format!(
419 "{}.{column}",
420 definition.table
421 )));
422 }
423 if !seen_update_columns.insert(column) {
424 return Err(SQLError::Routine {
425 sqlstate: "42701".into(),
426 message: format!("column \"{column}\" specified more than once"),
427 });
428 }
429 }
430 let mut condition_routine_bindings_changed = false;
431 if let Some(mut condition) = definition.when.take() {
432 condition_routine_bindings_changed =
433 self.validate_trigger_condition(definition, &columns, &mut condition)?;
434 definition.when = Some(condition);
435 }
436 Ok((relation, condition_routine_bindings_changed))
437 }
438
439 fn validate_trigger_transition_relations(
440 &self,
441 definition: &CreateTrigger,
442 relation: &RelationIdentity,
443 ) -> Result<(), SQLError> {
444 if definition.transition_relations.is_empty() {
445 return Ok(());
446 }
447 let hierarchy = self
448 .catalog
449 .loaded_table_hierarchy(relation)
450 .ok_or_else(|| {
451 SQLError::Internal(format!(
452 "trigger table `{}` disappeared during validation",
453 definition.table
454 ))
455 })?;
456 if definition.row && hierarchy.partition_spec.is_some() {
457 return Err(SQLError::Routine {
458 sqlstate: "0A000".into(),
459 message: format!("\"{}\" is a partitioned table", relation.name),
460 });
461 }
462 let mut old_table = None;
463 let mut new_table = None;
464 for transition in &definition.transition_relations {
465 validate_trigger_transition_relation(definition, &hierarchy, transition)?;
466 let duplicate = if transition.is_new {
467 new_table.replace(transition.name.as_str())
468 } else {
469 old_table.replace(transition.name.as_str())
470 };
471 if duplicate.is_some() {
472 return Err(SQLError::Routine {
473 sqlstate: "42P17".into(),
474 message: format!(
475 "{} TABLE cannot be specified multiple times",
476 if transition.is_new { "NEW" } else { "OLD" }
477 ),
478 });
479 }
480 }
481 if old_table.is_some() && old_table == new_table {
482 return Err(SQLError::Routine {
483 sqlstate: "42P17".into(),
484 message: "OLD TABLE name and NEW TABLE name cannot be the same".into(),
485 });
486 }
487 Ok(())
488 }
489
490 fn validate_trigger_condition(
491 &self,
492 definition: &CreateTrigger,
493 columns: &[crate::ast::ColumnDef],
494 condition: &mut Expr,
495 ) -> Result<bool, SQLError> {
496 validate_trigger_condition_references(definition, columns, condition)?;
497 let bound = bind_expr(condition, &mut TriggerConditionTypeResolver { columns })?;
498 let lowered = crate::plan::ExpressionPlan::lower_with(bound, &|name: &str| {
499 self.routines.has_registered_aggregate_function(name)
500 });
501 let mut plan = lowered.clone();
502 let ty =
503 self.stored_routines
504 .bind_expression(&mut plan, &[], &crate::RowSchema::default())?;
505 let sites = crate::binding::syntax_sites::expression_syntax_sites(&lowered, &plan)?;
507 let changed = crate::catalog::stored_ast::bind_stored_expression_sites(condition, &sites)?;
508 if !ty.as_ref().is_some_and(is_boolean_type) {
509 if let Expr::Literal(value @ (Value::Str(_) | Value::FixedChar(_))) = condition {
510 *value = crate::expr::cast_value(value, "boolean")?;
511 } else if let Some(ty) = ty {
512 return Err(SQLError::TypeMismatch(format!(
513 "argument of WHEN must be type boolean, not type {}",
514 ty.sql_name()
515 )));
516 } else {
517 *condition = Expr::Cast {
518 implicit: true,
519 expr: Box::new(condition.clone()),
520 ty: "boolean".into(),
521 };
522 }
523 }
524 crate::catalog::regrole_dependencies::reject_stored_regrole_constants_with(
525 self.regroles,
526 condition,
527 None,
528 )?;
529 Ok(changed)
530 }
531}
532
533fn trigger_column(name: String, ty: ColumnType) -> ColumnDef {
534 ColumnDef {
535 name,
536 ty,
537 object_id: None,
538 attribute_number: None,
539 missing_value: None,
540 primary_key: false,
541 not_null: false,
542 not_null_explicit: false,
543 not_null_name: None,
544 not_null_identity: None,
545 not_null_validated: true,
546 not_null_no_inherit: false,
547 not_null_is_local: true,
548 auto_increment: None,
549 unique: false,
550 default: None,
551 generated: None,
552 check: None,
553 check_name: None,
554 check_enforced: true,
555 check_validated: true,
556 check_no_inherit: false,
557 check_is_local: true,
558 check_object_id: None,
559 check_catalog_oid: None,
560 default_catalog_oid: None,
561 references: None,
562 }
563}