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