Skip to main content

uqa_sql/catalog/events/
validation.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Trigger condition and rule pseudo-relation validation over SQL metadata.
8
9use crate::{
10    ast::{
11        ColumnType, CreateTrigger, Expr, FromClause, OnConflictAction, RuleEvent, SelectStmt,
12        Statement, TableHierarchy, TriggerEvent, TriggerTiming, TriggerTransitionRelation,
13    },
14    plpgsql::{ResolvedVariable, VariableResolver},
15    semantics::rules::action_binding::{
16        first_rule_row_reference_in_expr, first_rule_row_reference_in_select, RuleSourceCatalog,
17    },
18    SQLError,
19};
20use uqa_core::Value;
21
22pub struct TriggerConditionTypeResolver<'a> {
23    pub columns: &'a [crate::ast::ColumnDef],
24}
25
26pub struct RuleRowTypeResolver<'a> {
27    pub columns: &'a [(String, ColumnType)],
28    pub event: RuleEvent,
29}
30
31pub struct RuleConditionNameResolver<'a> {
32    pub columns: &'a [(String, ColumnType)],
33    pub event: RuleEvent,
34}
35
36impl VariableResolver for RuleConditionNameResolver<'_> {
37    fn resolve_name(&mut self, _name: &str) -> Result<Option<ResolvedVariable>, SQLError> {
38        Ok(None)
39    }
40
41    fn resolve_qualified(
42        &mut self,
43        _qualifier: &str,
44        _column: &str,
45    ) -> Result<Option<ResolvedVariable>, SQLError> {
46        Ok(None)
47    }
48
49    fn resolve_param(&mut self, _index: usize) -> Result<Option<ResolvedVariable>, SQLError> {
50        Ok(None)
51    }
52
53    fn rewrite_name(&mut self, name: &str) -> Result<Option<Expr>, SQLError> {
54        if !self.columns.iter().any(|(column, _)| column == name) {
55            if name.eq_ignore_ascii_case("old") || name.eq_ignore_ascii_case("new") {
56                return Ok(None);
57            }
58            return Err(SQLError::UnknownColumn(name.to_string()));
59        }
60        let qualifier = match self.event {
61            RuleEvent::Insert => "new",
62            RuleEvent::Delete => "old",
63            RuleEvent::Update => return Err(SQLError::AmbiguousColumn(name.to_string())),
64            RuleEvent::Select => return Ok(None),
65        };
66        Ok(Some(Expr::qualified_column(qualifier, name)))
67    }
68}
69
70impl RuleRowTypeResolver<'_> {
71    fn validate_row_qualifier(&self, qualifier: &str) -> Result<bool, SQLError> {
72        let is_old = qualifier.eq_ignore_ascii_case("old");
73        let is_new = qualifier.eq_ignore_ascii_case("new");
74        if !is_old && !is_new {
75            return Ok(false);
76        }
77        if is_old && matches!(self.event, RuleEvent::Insert | RuleEvent::Select) {
78            return Err(SQLError::Routine {
79                sqlstate: "42P17".into(),
80                message: format!(
81                    "there is no OLD relation for {event} rule",
82                    event = rule_event_name(self.event)
83                ),
84            });
85        }
86        if is_new && matches!(self.event, RuleEvent::Delete | RuleEvent::Select) {
87            return Err(SQLError::Routine {
88                sqlstate: "42P17".into(),
89                message: format!(
90                    "there is no NEW relation for {event} rule",
91                    event = rule_event_name(self.event)
92                ),
93            });
94        }
95        Ok(true)
96    }
97
98    fn resolve_record_field(
99        &self,
100        qualifier: &str,
101        column: &str,
102    ) -> Result<Option<ResolvedVariable>, SQLError> {
103        if !self.validate_row_qualifier(qualifier)? {
104            return Ok(None);
105        }
106        let (_, ty) = self
107            .columns
108            .iter()
109            .find(|(name, _)| name == column)
110            .ok_or_else(|| SQLError::UnknownColumn(format!("{qualifier}.{column}")))?;
111        Ok(Some(ResolvedVariable {
112            value: Value::Null,
113            declared_type: Some(ty.catalog_name()),
114        }))
115    }
116}
117
118impl VariableResolver for RuleRowTypeResolver<'_> {
119    fn resolve_name(&mut self, name: &str) -> Result<Option<ResolvedVariable>, SQLError> {
120        Ok(self
121            .validate_row_qualifier(name)?
122            .then(|| ResolvedVariable::untyped(Value::Record(Vec::new()))))
123    }
124
125    fn resolve_qualified(
126        &mut self,
127        qualifier: &str,
128        column: &str,
129    ) -> Result<Option<ResolvedVariable>, SQLError> {
130        self.resolve_record_field(qualifier, column)
131    }
132
133    fn resolve_param(&mut self, _index: usize) -> Result<Option<ResolvedVariable>, SQLError> {
134        Ok(None)
135    }
136
137    fn rewrite_qualified_whole_row(&mut self, qualifier: &str) -> Result<Option<Expr>, SQLError> {
138        Ok(self
139            .validate_row_qualifier(qualifier)?
140            .then(|| Expr::Literal(Value::Record(Vec::new()))))
141    }
142}
143
144const fn rule_event_name(event: RuleEvent) -> &'static str {
145    match event {
146        RuleEvent::Select => "SELECT",
147        RuleEvent::Insert => "INSERT",
148        RuleEvent::Update => "UPDATE",
149        RuleEvent::Delete => "DELETE",
150    }
151}
152
153impl VariableResolver for TriggerConditionTypeResolver<'_> {
154    fn resolve_name(&mut self, _name: &str) -> Result<Option<ResolvedVariable>, SQLError> {
155        Ok(None)
156    }
157
158    fn resolve_qualified(
159        &mut self,
160        qualifier: &str,
161        column: &str,
162    ) -> Result<Option<ResolvedVariable>, SQLError> {
163        if !qualifier.eq_ignore_ascii_case("old") && !qualifier.eq_ignore_ascii_case("new") {
164            return Ok(None);
165        }
166        Ok(self
167            .columns
168            .iter()
169            .find(|definition| definition.name == column)
170            .map(|definition| ResolvedVariable {
171                value: Value::Null,
172                declared_type: Some(definition.ty.catalog_name()),
173            }))
174    }
175
176    fn resolve_param(&mut self, _index: usize) -> Result<Option<ResolvedVariable>, SQLError> {
177        Ok(None)
178    }
179}
180
181pub fn is_boolean_type(ty: &ColumnType) -> bool {
182    match ty {
183        ColumnType::Boolean => true,
184        ColumnType::Domain { base, .. } => is_boolean_type(base),
185        _ => false,
186    }
187}
188
189pub fn rule_action_has_returning(action: &Statement) -> bool {
190    match action {
191        Statement::Insert(statement) => !statement.returning.is_empty(),
192        Statement::Update(statement) => !statement.returning.is_empty(),
193        Statement::Delete(statement) => !statement.returning.is_empty(),
194        _ => false,
195    }
196}
197
198fn same_rule_returning_type_with_different_modifier(
199    actual: &ColumnType,
200    expected: &ColumnType,
201) -> bool {
202    match (actual, expected) {
203        (ColumnType::Varchar(_), ColumnType::Varchar(_))
204        | (ColumnType::Character(_), ColumnType::Character(_))
205        | (ColumnType::Numeric { .. }, ColumnType::Numeric { .. })
206        | (ColumnType::Vector(_), ColumnType::Vector(_))
207        | (ColumnType::Tensor(_), ColumnType::Tensor(_)) => true,
208        (ColumnType::Array(actual), ColumnType::Array(expected)) => {
209            same_rule_returning_type_with_different_modifier(actual, expected)
210        }
211        _ => false,
212    }
213}
214
215pub fn validate_rule_returning_shape(
216    schema: &crate::RowSchema,
217    columns: &[(String, ColumnType)],
218) -> Result<(), SQLError> {
219    if schema.len() < columns.len() {
220        return Err(SQLError::Routine {
221            sqlstate: "42P17".into(),
222            message: "RETURNING list has too few entries".into(),
223        });
224    }
225    if schema.len() > columns.len() {
226        return Err(SQLError::Routine {
227            sqlstate: "42P17".into(),
228            message: "RETURNING list has too many entries".into(),
229        });
230    }
231    for (position, (column, expected)) in columns.iter().enumerate() {
232        let Some(actual) = schema.column_type(position) else {
233            // PostgreSQL resolves an unknown literal against the event row's
234            // declared type when the rule target list is installed.
235            continue;
236        };
237        if actual == expected {
238            continue;
239        }
240        let difference = if same_rule_returning_type_with_different_modifier(actual, expected) {
241            "size"
242        } else {
243            "type"
244        };
245        return Err(SQLError::Diagnostic {
246            sqlstate: "42P17".into(),
247            message: format!(
248                "RETURNING list's entry {} has different {difference} from column \"{column}\"",
249                position + 1
250            ),
251            detail: Some(format!(
252                "RETURNING list entry has type {}, but column has type {}.",
253                actual.sql_name(),
254                expected.sql_name()
255            )),
256            hint: None,
257        });
258    }
259    Ok(())
260}
261
262pub fn validate_trigger_condition_references(
263    definition: &CreateTrigger,
264    columns: &[crate::ast::ColumnDef],
265    condition: &Expr,
266) -> Result<(), SQLError> {
267    if condition.any_node(&|node| {
268        matches!(
269            node,
270            Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. }
271        )
272    }) {
273        return Err(SQLError::Routine {
274            sqlstate: "0A000".into(),
275            message: "cannot use subquery in trigger WHEN condition".into(),
276        });
277    }
278    if condition.any_node(&|node| matches!(node, Expr::Column(_))) {
279        return Err(SQLError::Routine {
280            sqlstate: "42P01".into(),
281            message: "trigger WHEN condition must qualify row columns with OLD or NEW".into(),
282        });
283    }
284    let references_old = condition.any_node(&|node| {
285        matches!(node, Expr::QualifiedColumn { qualifier, .. } if qualifier.eq_ignore_ascii_case("old"))
286    });
287    let references_new = condition.any_node(&|node| {
288        matches!(node, Expr::QualifiedColumn { qualifier, .. } if qualifier.eq_ignore_ascii_case("new"))
289    });
290    if !definition.row && (references_old || references_new) {
291        return Err(SQLError::Routine {
292            sqlstate: "42P01".into(),
293            message: "statement trigger's WHEN condition cannot reference row values".into(),
294        });
295    }
296    if references_old && definition.events.contains(&TriggerEvent::Insert) {
297        return Err(SQLError::Routine {
298            sqlstate: "42P17".into(),
299            message: "INSERT trigger's WHEN condition cannot reference OLD values".into(),
300        });
301    }
302    if references_new && definition.events.contains(&TriggerEvent::Delete) {
303        return Err(SQLError::Routine {
304            sqlstate: "42P17".into(),
305            message: "DELETE trigger's WHEN condition cannot reference NEW values".into(),
306        });
307    }
308    let invalid_qualified_reference = std::cell::RefCell::new(None);
309    let _ = condition.any_node(&|node| {
310        let Expr::QualifiedColumn { qualifier, column } = node else {
311            return false;
312        };
313        if !qualifier.eq_ignore_ascii_case("old") && !qualifier.eq_ignore_ascii_case("new") {
314            *invalid_qualified_reference.borrow_mut() = Some(format!("{qualifier}.{column}"));
315            return true;
316        }
317        if !columns.iter().any(|definition| definition.name == *column) {
318            *invalid_qualified_reference.borrow_mut() = Some(column.clone());
319            return true;
320        }
321        false
322    });
323    if let Some(reference) = invalid_qualified_reference.into_inner() {
324        return Err(SQLError::UnknownColumn(reference));
325    }
326    if definition.timing == TriggerTiming::Before && references_new {
327        let generated = columns
328            .iter()
329            .filter(|column| column.generated.is_some())
330            .map(|column| column.name.as_str())
331            .collect::<std::collections::BTreeSet<_>>();
332        if condition.any_node(&|node| {
333            matches!(
334                node,
335                Expr::QualifiedColumn { qualifier, column }
336                    if qualifier.eq_ignore_ascii_case("new")
337                        && generated.contains(column.as_str())
338            )
339        }) {
340            return Err(SQLError::Routine {
341                sqlstate: "42P17".into(),
342                message: "BEFORE trigger's WHEN condition cannot reference NEW generated columns"
343                    .into(),
344            });
345        }
346    }
347    Ok(())
348}
349
350pub fn first_invalid_rule_condition_qualifier(condition: &Expr) -> Option<String> {
351    let invalid = std::cell::RefCell::new(None);
352    let _ = condition.any_node(&|node| {
353        let Expr::QualifiedColumn { qualifier, .. } = node else {
354            return false;
355        };
356        if qualifier.eq_ignore_ascii_case("old") || qualifier.eq_ignore_ascii_case("new") {
357            return false;
358        }
359        *invalid.borrow_mut() = Some(qualifier.clone());
360        true
361    });
362    invalid.into_inner()
363}
364
365fn invalid_rule_cte_reference(qualifier: &str) -> SQLError {
366    SQLError::Routine {
367        sqlstate: "0A000".into(),
368        message: format!(
369            "cannot refer to {} within WITH query",
370            qualifier.to_ascii_uppercase()
371        ),
372    }
373}
374
375fn invalid_rule_set_operation_reference() -> SQLError {
376    SQLError::Routine {
377        sqlstate: "42P10".into(),
378        message:
379            "UNION/INTERSECT/EXCEPT member statement cannot refer to other relations of same query level"
380                .into(),
381    }
382}
383
384pub(super) use crate::semantics::rules::action_binding::invalid_rule_action_reference;
385
386fn ambiguous_rule_pseudo_relation(qualifier: &str) -> SQLError {
387    SQLError::Routine {
388        sqlstate: "42P09".into(),
389        message: format!("table reference \"{qualifier}\" is ambiguous"),
390    }
391}
392
393fn duplicate_rule_pseudo_relation(qualifier: &str) -> SQLError {
394    SQLError::Routine {
395        sqlstate: "42712".into(),
396        message: format!("table name \"{qualifier}\" specified more than once"),
397    }
398}
399
400fn rule_pseudo_relation_name(name: &str) -> Option<String> {
401    (name.eq_ignore_ascii_case("old") || name.eq_ignore_ascii_case("new"))
402        .then(|| name.to_ascii_lowercase())
403}
404
405fn first_rule_pseudo_relation_in_from(from: &FromClause) -> Option<String> {
406    match from {
407        FromClause::Table {
408            name,
409            qualifier,
410            alias,
411            ..
412        } => alias
413            .as_deref()
414            .and_then(rule_pseudo_relation_name)
415            .or_else(|| rule_pseudo_relation_name(qualifier))
416            .or_else(|| rule_pseudo_relation_name(name))
417            .or_else(|| {
418                name.rsplit_once('.')
419                    .and_then(|(_, local)| rule_pseudo_relation_name(local.trim_matches('"')))
420            }),
421        FromClause::Join {
422            left, right, alias, ..
423        } => alias
424            .as_deref()
425            .and_then(rule_pseudo_relation_name)
426            .or_else(|| first_rule_pseudo_relation_in_from(left))
427            .or_else(|| first_rule_pseudo_relation_in_from(right)),
428        FromClause::Values { alias, .. } | FromClause::Subquery { alias, .. } => {
429            alias.as_deref().and_then(rule_pseudo_relation_name)
430        }
431        FromClause::Function {
432            output_name, alias, ..
433        } => rule_pseudo_relation_name(alias.as_deref().unwrap_or(output_name)),
434        FromClause::FunctionGroup {
435            functions, alias, ..
436        } => alias
437            .as_deref()
438            .and_then(rule_pseudo_relation_name)
439            .or_else(|| {
440                functions
441                    .iter()
442                    .find_map(|function| rule_pseudo_relation_name(&function.output_name))
443            }),
444    }
445}
446
447fn validate_rule_action_select_namespace(select: &SelectStmt) -> Result<(), SQLError> {
448    let duplicate = select
449        .with
450        .iter()
451        .find_map(|cte| rule_pseudo_relation_name(&cte.name))
452        .or_else(|| {
453            select
454                .from
455                .as_ref()
456                .and_then(first_rule_pseudo_relation_in_from)
457        });
458    if let Some(qualifier) = duplicate {
459        return Err(duplicate_rule_pseudo_relation(&qualifier));
460    }
461    Ok(())
462}
463
464fn validate_rule_action_namespace(
465    catalog: &dyn RuleSourceCatalog,
466    action: &Statement,
467) -> Result<(), SQLError> {
468    let (ctes, source) = match action {
469        Statement::Select(select) => return validate_rule_action_select_namespace(select),
470        Statement::Insert(insert) => (insert.with.as_slice(), insert.select_source.as_deref()),
471        Statement::Update(update) => {
472            if let Some(qualifier) = update
473                .from
474                .as_ref()
475                .and_then(first_rule_pseudo_relation_in_from)
476            {
477                return Err(duplicate_rule_pseudo_relation(&qualifier));
478            }
479            if let Some(qualifier) = rule_pseudo_relation_name(&update.target_qualifier) {
480                if crate::semantics::rules::action_binding::action_target_qualifier_referenced(
481                    catalog, action, &qualifier,
482                ) {
483                    return Err(ambiguous_rule_pseudo_relation(&qualifier));
484                }
485            }
486            (update.with.as_slice(), None)
487        }
488        Statement::Delete(delete) => {
489            if let Some(qualifier) = delete
490                .using
491                .as_ref()
492                .and_then(first_rule_pseudo_relation_in_from)
493            {
494                return Err(duplicate_rule_pseudo_relation(&qualifier));
495            }
496            if let Some(qualifier) = rule_pseudo_relation_name(&delete.target_qualifier) {
497                if crate::semantics::rules::action_binding::action_target_qualifier_referenced(
498                    catalog, action, &qualifier,
499                ) {
500                    return Err(ambiguous_rule_pseudo_relation(&qualifier));
501                }
502            }
503            (delete.with.as_slice(), None)
504        }
505        _ => return Ok(()),
506    };
507    if let Some(qualifier) = ctes
508        .iter()
509        .find_map(|cte| rule_pseudo_relation_name(&cte.name))
510    {
511        return Err(duplicate_rule_pseudo_relation(&qualifier));
512    }
513    if let Some(select) = source {
514        validate_rule_action_select_namespace(select)?;
515    }
516    Ok(())
517}
518
519fn validate_rule_ctes(
520    catalog: &dyn RuleSourceCatalog,
521    ctes: &[crate::ast::CTE],
522) -> Result<(), SQLError> {
523    for cte in ctes {
524        let statement = cte.body.clone().into_statement();
525        if let Some(qualifier) =
526            crate::semantics::rules::action_binding::first_rule_row_reference_in_statement(
527                catalog, &statement,
528            )?
529        {
530            return Err(invalid_rule_cte_reference(&qualifier));
531        }
532        validate_rule_action_reference_scopes(catalog, &statement)?;
533    }
534    Ok(())
535}
536
537fn validate_rule_select_scopes(
538    catalog: &dyn RuleSourceCatalog,
539    select: &SelectStmt,
540) -> Result<(), SQLError> {
541    validate_rule_ctes(catalog, &select.with)?;
542    if let Some(set_op) = &select.set_op {
543        let member_references_rule_row = set_op
544            .left
545            .as_deref()
546            .and_then(|left| first_rule_row_reference_in_select(catalog, left))
547            .or_else(|| first_rule_row_reference_in_select(catalog, &set_op.right));
548        if member_references_rule_row.is_some() {
549            return Err(invalid_rule_set_operation_reference());
550        }
551        if let Some(left) = set_op.left.as_deref() {
552            validate_rule_select_scopes(catalog, left)?;
553        }
554        validate_rule_select_scopes(catalog, &set_op.right)?;
555        for order in &set_op.combined_order_by {
556            validate_rule_expr_scopes(catalog, &order.expr)?;
557        }
558        if let Some(limit) = &set_op.combined_limit {
559            validate_rule_expr_scopes(catalog, limit)?;
560        }
561        if let Some(offset) = &set_op.combined_offset {
562            validate_rule_expr_scopes(catalog, offset)?;
563        }
564    }
565    for projection in &select.projections {
566        validate_rule_expr_scopes(catalog, &projection.expr)?;
567    }
568    for expr in select.values.iter().flatten() {
569        validate_rule_expr_scopes(catalog, expr)?;
570    }
571    if let Some(from) = &select.from {
572        validate_rule_from_scopes(catalog, from)?;
573    }
574    for expr in select
575        .r#where
576        .iter()
577        .chain(select.group_by.iter())
578        .chain(select.grouping_sets.iter().flatten())
579        .chain(select.having.iter())
580        .chain(select.order_by.iter().map(|order| &order.expr))
581        .chain(select.limit.iter())
582        .chain(select.offset.iter())
583        .chain(select.distinct_on.iter())
584    {
585        validate_rule_expr_scopes(catalog, expr)?;
586    }
587    Ok(())
588}
589
590fn validate_rule_from_scopes(
591    catalog: &dyn RuleSourceCatalog,
592    from: &FromClause,
593) -> Result<(), SQLError> {
594    match from {
595        FromClause::Table { .. } => {}
596        FromClause::Join {
597            left, right, on, ..
598        } => {
599            validate_rule_from_scopes(catalog, left)?;
600            validate_rule_from_scopes(catalog, right)?;
601            if let Some(on) = on {
602                validate_rule_expr_scopes(catalog, on)?;
603            }
604        }
605        FromClause::Values { rows, .. } => {
606            for expr in rows.iter().flatten() {
607                validate_rule_expr_scopes(catalog, expr)?;
608            }
609        }
610        FromClause::Function { args, .. } => {
611            for expr in args {
612                validate_rule_expr_scopes(catalog, expr)?;
613            }
614        }
615        FromClause::FunctionGroup { functions, .. } => {
616            for expr in functions.iter().flat_map(|function| &function.args) {
617                validate_rule_expr_scopes(catalog, expr)?;
618            }
619        }
620        FromClause::Subquery { body, .. } => validate_rule_select_scopes(catalog, body)?,
621    }
622    Ok(())
623}
624
625fn validate_rule_expr_scopes(catalog: &dyn RuleSourceCatalog, expr: &Expr) -> Result<(), SQLError> {
626    match expr {
627        Expr::Func {
628            args,
629            order_by,
630            filter,
631            ..
632        } => {
633            for expr in args {
634                validate_rule_expr_scopes(catalog, expr)?;
635            }
636            for order in order_by {
637                validate_rule_expr_scopes(catalog, &order.expr)?;
638            }
639            if let Some(filter) = filter {
640                validate_rule_expr_scopes(catalog, filter)?;
641            }
642        }
643        Expr::Array(items)
644        | Expr::Row(items)
645        | Expr::CompositeRow { items, .. }
646        | Expr::And(items)
647        | Expr::Or(items) => {
648            for expr in items {
649                validate_rule_expr_scopes(catalog, expr)?;
650            }
651        }
652        Expr::Binary { lhs, rhs, .. } => {
653            validate_rule_expr_scopes(catalog, lhs)?;
654            validate_rule_expr_scopes(catalog, rhs)?;
655        }
656        Expr::UnaryMinus(expr)
657        | Expr::Not(expr)
658        | Expr::IsNull { expr, .. }
659        | Expr::Cast { expr, .. } => validate_rule_expr_scopes(catalog, expr)?,
660        Expr::Between { expr, low, high } => {
661            validate_rule_expr_scopes(catalog, expr)?;
662            validate_rule_expr_scopes(catalog, low)?;
663            validate_rule_expr_scopes(catalog, high)?;
664        }
665        Expr::InList { expr, list, .. } => {
666            validate_rule_expr_scopes(catalog, expr)?;
667            for item in list {
668                validate_rule_expr_scopes(catalog, item)?;
669            }
670        }
671        Expr::WindowCall {
672            args, spec, filter, ..
673        } => {
674            for expr in args
675                .iter()
676                .chain(filter.iter().map(AsRef::as_ref))
677                .chain(spec.partition_by.iter())
678            {
679                validate_rule_expr_scopes(catalog, expr)?;
680            }
681            for order in &spec.order_by {
682                validate_rule_expr_scopes(catalog, &order.expr)?;
683            }
684        }
685        Expr::Case {
686            base,
687            when,
688            else_branch,
689        } => {
690            if let Some(base) = base {
691                validate_rule_expr_scopes(catalog, base)?;
692            }
693            for (condition, result) in when {
694                validate_rule_expr_scopes(catalog, condition)?;
695                validate_rule_expr_scopes(catalog, result)?;
696            }
697            if let Some(else_branch) = else_branch {
698                validate_rule_expr_scopes(catalog, else_branch)?;
699            }
700        }
701        Expr::ScalarSubquery(body) | Expr::Exists { body, .. } => {
702            validate_rule_select_scopes(catalog, body)?;
703        }
704        Expr::InSubquery { expr, body, .. } => {
705            validate_rule_expr_scopes(catalog, expr)?;
706            validate_rule_select_scopes(catalog, body)?;
707        }
708        Expr::Default
709        | Expr::Literal(_)
710        | Expr::TypedLiteral { .. }
711        | Expr::Star
712        | Expr::QualifiedStar(_)
713        | Expr::Column(_)
714        | Expr::QualifiedColumn { .. }
715        | Expr::InternalColumn(_)
716        | Expr::Param(_) => {}
717    }
718    Ok(())
719}
720
721pub fn validate_rule_action_reference_scopes(
722    catalog: &dyn RuleSourceCatalog,
723    action: &Statement,
724) -> Result<(), SQLError> {
725    validate_rule_action_namespace(catalog, action)?;
726    match action {
727        Statement::Select(select) => validate_rule_select_scopes(catalog, select),
728        Statement::Insert(insert) => {
729            validate_rule_ctes(catalog, &insert.with)?;
730            for expr in insert
731                .columns
732                .iter()
733                .flat_map(crate::ast::AssignmentTarget::expressions)
734                .chain(insert.rows.iter().flatten())
735            {
736                validate_rule_expr_scopes(catalog, expr)?;
737            }
738            if let Some(select) = &insert.select_source {
739                validate_rule_select_scopes(catalog, select)?;
740            }
741            if let Some(conflict) = &insert.on_conflict {
742                for expression in &conflict.expressions {
743                    validate_rule_expr_scopes(catalog, expression)?;
744                }
745                if let Some(predicate) = conflict.predicate.as_deref() {
746                    validate_rule_expr_scopes(catalog, predicate)?;
747                }
748                if let OnConflictAction::Update {
749                    assignments,
750                    r#where,
751                } = &conflict.action
752                {
753                    let reference = assignments
754                        .iter()
755                        .flat_map(|(target, value)| {
756                            target.expressions().chain(std::iter::once(value))
757                        })
758                        .find_map(|expr| {
759                            let mut shadowed = std::collections::BTreeSet::new();
760                            shadowed.insert(insert.target_qualifier.to_ascii_lowercase());
761                            first_rule_row_reference_in_expr(expr, &shadowed)
762                        })
763                        .or_else(|| {
764                            r#where.as_ref().and_then(|expr| {
765                                let mut shadowed = std::collections::BTreeSet::new();
766                                shadowed.insert(insert.target_qualifier.to_ascii_lowercase());
767                                first_rule_row_reference_in_expr(expr, &shadowed)
768                            })
769                        });
770                    if let Some(qualifier) = reference {
771                        return Err(invalid_rule_action_reference(&qualifier));
772                    }
773                    for expr in assignments.iter().flat_map(|(target, value)| {
774                        target.expressions().chain(std::iter::once(value))
775                    }) {
776                        validate_rule_expr_scopes(catalog, expr)?;
777                    }
778                    if let Some(r#where) = r#where {
779                        validate_rule_expr_scopes(catalog, r#where)?;
780                    }
781                }
782            }
783            for projection in &insert.returning {
784                validate_rule_expr_scopes(catalog, &projection.expr)?;
785            }
786            Ok(())
787        }
788        Statement::Update(update) => {
789            validate_rule_ctes(catalog, &update.with)?;
790            if let Some(from) = &update.from {
791                validate_rule_from_scopes(catalog, from)?;
792            }
793            for expr in update
794                .assignments
795                .iter()
796                .flat_map(|(target, value)| target.expressions().chain(std::iter::once(value)))
797                .chain(update.r#where.iter())
798                .chain(update.returning.iter().map(|projection| &projection.expr))
799            {
800                validate_rule_expr_scopes(catalog, expr)?;
801            }
802            Ok(())
803        }
804        Statement::Delete(delete) => {
805            validate_rule_ctes(catalog, &delete.with)?;
806            if let Some(using) = &delete.using {
807                validate_rule_from_scopes(catalog, using)?;
808            }
809            for expr in delete
810                .r#where
811                .iter()
812                .chain(delete.returning.iter().map(|projection| &projection.expr))
813            {
814                validate_rule_expr_scopes(catalog, expr)?;
815            }
816            Ok(())
817        }
818        _ => Ok(()),
819    }
820}
821
822pub fn validate_trigger_transition_relation(
823    definition: &CreateTrigger,
824    hierarchy: &TableHierarchy,
825    transition: &TriggerTransitionRelation,
826) -> Result<(), SQLError> {
827    if !transition.is_table {
828        return Err(SQLError::Routine {
829            sqlstate: "0A000".into(),
830            message: "ROW variable naming in the REFERENCING clause is not supported".into(),
831        });
832    }
833    if definition.row && !hierarchy.parents.is_empty() {
834        return Err(SQLError::Routine {
835            sqlstate: "0A000".into(),
836            message: if hierarchy.partition_bound.is_some() {
837                "ROW triggers with transition tables are not supported on partitions".into()
838            } else {
839                "ROW triggers with transition tables are not supported on inheritance children"
840                    .into()
841            },
842        });
843    }
844    if definition.timing != TriggerTiming::After {
845        return Err(SQLError::Routine {
846            sqlstate: "42P17".into(),
847            message: "transition table name can only be specified for an AFTER trigger".into(),
848        });
849    }
850    if definition.events.contains(&TriggerEvent::Truncate) {
851        return Err(SQLError::Routine {
852            sqlstate: "0A000".into(),
853            message: "TRUNCATE triggers with transition tables are not supported".into(),
854        });
855    }
856    let mutation_events = definition
857        .events
858        .iter()
859        .filter(|event| {
860            matches!(
861                event,
862                TriggerEvent::Insert | TriggerEvent::Update | TriggerEvent::Delete
863            )
864        })
865        .count();
866    if mutation_events != 1 {
867        return Err(SQLError::Routine {
868            sqlstate: "0A000".into(),
869            message: "transition tables cannot be specified for triggers with more than one event"
870                .into(),
871        });
872    }
873    if !definition.update_columns.is_empty() {
874        return Err(SQLError::Routine {
875            sqlstate: "0A000".into(),
876            message: "transition tables cannot be specified for triggers with column lists".into(),
877        });
878    }
879    let valid_event = definition.events.iter().any(|event| {
880        if transition.is_new {
881            matches!(event, TriggerEvent::Insert | TriggerEvent::Update)
882        } else {
883            matches!(event, TriggerEvent::Delete | TriggerEvent::Update)
884        }
885    });
886    if !valid_event {
887        return Err(SQLError::Routine {
888            sqlstate: "42P17".into(),
889            message: format!(
890                "{} TABLE can only be specified for {} trigger",
891                if transition.is_new { "NEW" } else { "OLD" },
892                if transition.is_new {
893                    "an INSERT or UPDATE"
894                } else {
895                    "a DELETE or UPDATE"
896                }
897            ),
898        });
899    }
900    Ok(())
901}
902
903#[cfg(test)]
904mod tests;