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.sql_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.sql_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::Routine {
246            sqlstate: "42P17".into(),
247            message: format!(
248                "RETURNING list's entry {} has different {difference} from column \"{column}\"\nDETAIL: RETURNING list entry has type {}, but column has type {}.",
249                position + 1,
250                actual.sql_name(),
251                expected.sql_name()
252            ),
253        });
254    }
255    Ok(())
256}
257
258pub fn validate_trigger_condition_references(
259    definition: &CreateTrigger,
260    columns: &[crate::ast::ColumnDef],
261    condition: &Expr,
262) -> Result<(), SQLError> {
263    if condition.any_node(&|node| {
264        matches!(
265            node,
266            Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. }
267        )
268    }) {
269        return Err(SQLError::Routine {
270            sqlstate: "0A000".into(),
271            message: "cannot use subquery in trigger WHEN condition".into(),
272        });
273    }
274    if condition.any_node(&|node| matches!(node, Expr::Column(_))) {
275        return Err(SQLError::Routine {
276            sqlstate: "42P01".into(),
277            message: "trigger WHEN condition must qualify row columns with OLD or NEW".into(),
278        });
279    }
280    let references_old = condition.any_node(&|node| {
281        matches!(node, Expr::QualifiedColumn { qualifier, .. } if qualifier.eq_ignore_ascii_case("old"))
282    });
283    let references_new = condition.any_node(&|node| {
284        matches!(node, Expr::QualifiedColumn { qualifier, .. } if qualifier.eq_ignore_ascii_case("new"))
285    });
286    if !definition.row && (references_old || references_new) {
287        return Err(SQLError::Routine {
288            sqlstate: "42P01".into(),
289            message: "statement trigger's WHEN condition cannot reference row values".into(),
290        });
291    }
292    if references_old && definition.events.contains(&TriggerEvent::Insert) {
293        return Err(SQLError::Routine {
294            sqlstate: "42P17".into(),
295            message: "INSERT trigger's WHEN condition cannot reference OLD values".into(),
296        });
297    }
298    if references_new && definition.events.contains(&TriggerEvent::Delete) {
299        return Err(SQLError::Routine {
300            sqlstate: "42P17".into(),
301            message: "DELETE trigger's WHEN condition cannot reference NEW values".into(),
302        });
303    }
304    let invalid_qualified_reference = std::cell::RefCell::new(None);
305    let _ = condition.any_node(&|node| {
306        let Expr::QualifiedColumn { qualifier, column } = node else {
307            return false;
308        };
309        if !qualifier.eq_ignore_ascii_case("old") && !qualifier.eq_ignore_ascii_case("new") {
310            *invalid_qualified_reference.borrow_mut() = Some(format!("{qualifier}.{column}"));
311            return true;
312        }
313        if !columns.iter().any(|definition| definition.name == *column) {
314            *invalid_qualified_reference.borrow_mut() = Some(column.clone());
315            return true;
316        }
317        false
318    });
319    if let Some(reference) = invalid_qualified_reference.into_inner() {
320        return Err(SQLError::UnknownColumn(reference));
321    }
322    if definition.timing == TriggerTiming::Before && references_new {
323        let generated = columns
324            .iter()
325            .filter(|column| column.generated.is_some())
326            .map(|column| column.name.as_str())
327            .collect::<std::collections::BTreeSet<_>>();
328        if condition.any_node(&|node| {
329            matches!(
330                node,
331                Expr::QualifiedColumn { qualifier, column }
332                    if qualifier.eq_ignore_ascii_case("new")
333                        && generated.contains(column.as_str())
334            )
335        }) {
336            return Err(SQLError::Routine {
337                sqlstate: "42P17".into(),
338                message: "BEFORE trigger's WHEN condition cannot reference NEW generated columns"
339                    .into(),
340            });
341        }
342    }
343    Ok(())
344}
345
346pub fn first_invalid_rule_condition_qualifier(condition: &Expr) -> Option<String> {
347    let invalid = std::cell::RefCell::new(None);
348    let _ = condition.any_node(&|node| {
349        let Expr::QualifiedColumn { qualifier, .. } = node else {
350            return false;
351        };
352        if qualifier.eq_ignore_ascii_case("old") || qualifier.eq_ignore_ascii_case("new") {
353            return false;
354        }
355        *invalid.borrow_mut() = Some(qualifier.clone());
356        true
357    });
358    invalid.into_inner()
359}
360
361fn invalid_rule_cte_reference(qualifier: &str) -> SQLError {
362    SQLError::Routine {
363        sqlstate: "0A000".into(),
364        message: format!(
365            "cannot refer to {} within WITH query",
366            qualifier.to_ascii_uppercase()
367        ),
368    }
369}
370
371fn invalid_rule_set_operation_reference() -> SQLError {
372    SQLError::Routine {
373        sqlstate: "42P10".into(),
374        message:
375            "UNION/INTERSECT/EXCEPT member statement cannot refer to other relations of same query level"
376                .into(),
377    }
378}
379
380pub(super) use crate::semantics::rules::action_binding::invalid_rule_action_reference;
381
382fn ambiguous_rule_pseudo_relation(qualifier: &str) -> SQLError {
383    SQLError::Routine {
384        sqlstate: "42P09".into(),
385        message: format!("table reference \"{qualifier}\" is ambiguous"),
386    }
387}
388
389fn duplicate_rule_pseudo_relation(qualifier: &str) -> SQLError {
390    SQLError::Routine {
391        sqlstate: "42712".into(),
392        message: format!("table name \"{qualifier}\" specified more than once"),
393    }
394}
395
396fn rule_pseudo_relation_name(name: &str) -> Option<String> {
397    (name.eq_ignore_ascii_case("old") || name.eq_ignore_ascii_case("new"))
398        .then(|| name.to_ascii_lowercase())
399}
400
401fn first_rule_pseudo_relation_in_from(from: &FromClause) -> Option<String> {
402    match from {
403        FromClause::Table {
404            name,
405            qualifier,
406            alias,
407            ..
408        } => alias
409            .as_deref()
410            .and_then(rule_pseudo_relation_name)
411            .or_else(|| rule_pseudo_relation_name(qualifier))
412            .or_else(|| rule_pseudo_relation_name(name))
413            .or_else(|| {
414                name.rsplit_once('.')
415                    .and_then(|(_, local)| rule_pseudo_relation_name(local.trim_matches('"')))
416            }),
417        FromClause::Join {
418            left, right, alias, ..
419        } => alias
420            .as_deref()
421            .and_then(rule_pseudo_relation_name)
422            .or_else(|| first_rule_pseudo_relation_in_from(left))
423            .or_else(|| first_rule_pseudo_relation_in_from(right)),
424        FromClause::Values { alias, .. } | FromClause::Subquery { alias, .. } => {
425            alias.as_deref().and_then(rule_pseudo_relation_name)
426        }
427        FromClause::Function {
428            output_name, alias, ..
429        } => rule_pseudo_relation_name(alias.as_deref().unwrap_or(output_name)),
430        FromClause::FunctionGroup {
431            functions, alias, ..
432        } => alias
433            .as_deref()
434            .and_then(rule_pseudo_relation_name)
435            .or_else(|| {
436                functions
437                    .iter()
438                    .find_map(|function| rule_pseudo_relation_name(&function.output_name))
439            }),
440    }
441}
442
443fn validate_rule_action_select_namespace(select: &SelectStmt) -> Result<(), SQLError> {
444    let duplicate = select
445        .with
446        .iter()
447        .find_map(|cte| rule_pseudo_relation_name(&cte.name))
448        .or_else(|| {
449            select
450                .from
451                .as_ref()
452                .and_then(first_rule_pseudo_relation_in_from)
453        });
454    if let Some(qualifier) = duplicate {
455        return Err(duplicate_rule_pseudo_relation(&qualifier));
456    }
457    Ok(())
458}
459
460fn validate_rule_action_namespace(
461    catalog: &dyn RuleSourceCatalog,
462    action: &Statement,
463) -> Result<(), SQLError> {
464    let (ctes, source) = match action {
465        Statement::Select(select) => return validate_rule_action_select_namespace(select),
466        Statement::Insert(insert) => (insert.with.as_slice(), insert.select_source.as_deref()),
467        Statement::Update(update) => {
468            if let Some(qualifier) = update
469                .from
470                .as_ref()
471                .and_then(first_rule_pseudo_relation_in_from)
472            {
473                return Err(duplicate_rule_pseudo_relation(&qualifier));
474            }
475            if let Some(qualifier) = rule_pseudo_relation_name(&update.target_qualifier) {
476                if crate::semantics::rules::action_binding::action_target_qualifier_referenced(
477                    catalog, action, &qualifier,
478                ) {
479                    return Err(ambiguous_rule_pseudo_relation(&qualifier));
480                }
481            }
482            (update.with.as_slice(), None)
483        }
484        Statement::Delete(delete) => {
485            if let Some(qualifier) = delete
486                .using
487                .as_ref()
488                .and_then(first_rule_pseudo_relation_in_from)
489            {
490                return Err(duplicate_rule_pseudo_relation(&qualifier));
491            }
492            if let Some(qualifier) = rule_pseudo_relation_name(&delete.target_qualifier) {
493                if crate::semantics::rules::action_binding::action_target_qualifier_referenced(
494                    catalog, action, &qualifier,
495                ) {
496                    return Err(ambiguous_rule_pseudo_relation(&qualifier));
497                }
498            }
499            (delete.with.as_slice(), None)
500        }
501        _ => return Ok(()),
502    };
503    if let Some(qualifier) = ctes
504        .iter()
505        .find_map(|cte| rule_pseudo_relation_name(&cte.name))
506    {
507        return Err(duplicate_rule_pseudo_relation(&qualifier));
508    }
509    if let Some(select) = source {
510        validate_rule_action_select_namespace(select)?;
511    }
512    Ok(())
513}
514
515fn validate_rule_ctes(
516    catalog: &dyn RuleSourceCatalog,
517    ctes: &[crate::ast::CTE],
518) -> Result<(), SQLError> {
519    for cte in ctes {
520        let statement = cte.body.clone().into_statement();
521        if let Some(qualifier) =
522            crate::semantics::rules::action_binding::first_rule_row_reference_in_statement(
523                catalog, &statement,
524            )?
525        {
526            return Err(invalid_rule_cte_reference(&qualifier));
527        }
528        validate_rule_action_reference_scopes(catalog, &statement)?;
529    }
530    Ok(())
531}
532
533fn validate_rule_select_scopes(
534    catalog: &dyn RuleSourceCatalog,
535    select: &SelectStmt,
536) -> Result<(), SQLError> {
537    validate_rule_ctes(catalog, &select.with)?;
538    if let Some(set_op) = &select.set_op {
539        let member_references_rule_row = set_op
540            .left
541            .as_deref()
542            .and_then(|left| first_rule_row_reference_in_select(catalog, left))
543            .or_else(|| first_rule_row_reference_in_select(catalog, &set_op.right));
544        if member_references_rule_row.is_some() {
545            return Err(invalid_rule_set_operation_reference());
546        }
547        if let Some(left) = set_op.left.as_deref() {
548            validate_rule_select_scopes(catalog, left)?;
549        }
550        validate_rule_select_scopes(catalog, &set_op.right)?;
551        for order in &set_op.combined_order_by {
552            validate_rule_expr_scopes(catalog, &order.expr)?;
553        }
554        if let Some(limit) = &set_op.combined_limit {
555            validate_rule_expr_scopes(catalog, limit)?;
556        }
557        if let Some(offset) = &set_op.combined_offset {
558            validate_rule_expr_scopes(catalog, offset)?;
559        }
560    }
561    for projection in &select.projections {
562        validate_rule_expr_scopes(catalog, &projection.expr)?;
563    }
564    for expr in select.values.iter().flatten() {
565        validate_rule_expr_scopes(catalog, expr)?;
566    }
567    if let Some(from) = &select.from {
568        validate_rule_from_scopes(catalog, from)?;
569    }
570    for expr in select
571        .r#where
572        .iter()
573        .chain(select.group_by.iter())
574        .chain(select.grouping_sets.iter().flatten())
575        .chain(select.having.iter())
576        .chain(select.order_by.iter().map(|order| &order.expr))
577        .chain(select.limit.iter())
578        .chain(select.offset.iter())
579        .chain(select.distinct_on.iter())
580    {
581        validate_rule_expr_scopes(catalog, expr)?;
582    }
583    Ok(())
584}
585
586fn validate_rule_from_scopes(
587    catalog: &dyn RuleSourceCatalog,
588    from: &FromClause,
589) -> Result<(), SQLError> {
590    match from {
591        FromClause::Table { .. } => {}
592        FromClause::Join {
593            left, right, on, ..
594        } => {
595            validate_rule_from_scopes(catalog, left)?;
596            validate_rule_from_scopes(catalog, right)?;
597            if let Some(on) = on {
598                validate_rule_expr_scopes(catalog, on)?;
599            }
600        }
601        FromClause::Values { rows, .. } => {
602            for expr in rows.iter().flatten() {
603                validate_rule_expr_scopes(catalog, expr)?;
604            }
605        }
606        FromClause::Function { args, .. } => {
607            for expr in args {
608                validate_rule_expr_scopes(catalog, expr)?;
609            }
610        }
611        FromClause::FunctionGroup { functions, .. } => {
612            for expr in functions.iter().flat_map(|function| &function.args) {
613                validate_rule_expr_scopes(catalog, expr)?;
614            }
615        }
616        FromClause::Subquery { body, .. } => validate_rule_select_scopes(catalog, body)?,
617    }
618    Ok(())
619}
620
621fn validate_rule_expr_scopes(catalog: &dyn RuleSourceCatalog, expr: &Expr) -> Result<(), SQLError> {
622    match expr {
623        Expr::Func {
624            args,
625            order_by,
626            filter,
627            ..
628        } => {
629            for expr in args {
630                validate_rule_expr_scopes(catalog, expr)?;
631            }
632            for order in order_by {
633                validate_rule_expr_scopes(catalog, &order.expr)?;
634            }
635            if let Some(filter) = filter {
636                validate_rule_expr_scopes(catalog, filter)?;
637            }
638        }
639        Expr::Array(items) | Expr::Row(items) | Expr::And(items) | Expr::Or(items) => {
640            for expr in items {
641                validate_rule_expr_scopes(catalog, expr)?;
642            }
643        }
644        Expr::Binary { lhs, rhs, .. } => {
645            validate_rule_expr_scopes(catalog, lhs)?;
646            validate_rule_expr_scopes(catalog, rhs)?;
647        }
648        Expr::UnaryMinus(expr)
649        | Expr::Not(expr)
650        | Expr::IsNull { expr, .. }
651        | Expr::Cast { expr, .. } => validate_rule_expr_scopes(catalog, expr)?,
652        Expr::Between { expr, low, high } => {
653            validate_rule_expr_scopes(catalog, expr)?;
654            validate_rule_expr_scopes(catalog, low)?;
655            validate_rule_expr_scopes(catalog, high)?;
656        }
657        Expr::InList { expr, list, .. } => {
658            validate_rule_expr_scopes(catalog, expr)?;
659            for item in list {
660                validate_rule_expr_scopes(catalog, item)?;
661            }
662        }
663        Expr::WindowCall { args, spec, .. } => {
664            for expr in args.iter().chain(spec.partition_by.iter()) {
665                validate_rule_expr_scopes(catalog, expr)?;
666            }
667            for order in &spec.order_by {
668                validate_rule_expr_scopes(catalog, &order.expr)?;
669            }
670        }
671        Expr::Case {
672            base,
673            when,
674            else_branch,
675        } => {
676            if let Some(base) = base {
677                validate_rule_expr_scopes(catalog, base)?;
678            }
679            for (condition, result) in when {
680                validate_rule_expr_scopes(catalog, condition)?;
681                validate_rule_expr_scopes(catalog, result)?;
682            }
683            if let Some(else_branch) = else_branch {
684                validate_rule_expr_scopes(catalog, else_branch)?;
685            }
686        }
687        Expr::ScalarSubquery(body) | Expr::Exists { body, .. } => {
688            validate_rule_select_scopes(catalog, body)?;
689        }
690        Expr::InSubquery { expr, body, .. } => {
691            validate_rule_expr_scopes(catalog, expr)?;
692            validate_rule_select_scopes(catalog, body)?;
693        }
694        Expr::Default
695        | Expr::Literal(_)
696        | Expr::TypedLiteral { .. }
697        | Expr::Star
698        | Expr::QualifiedStar(_)
699        | Expr::Column(_)
700        | Expr::QualifiedColumn { .. }
701        | Expr::InternalColumn(_)
702        | Expr::Param(_) => {}
703    }
704    Ok(())
705}
706
707pub fn validate_rule_action_reference_scopes(
708    catalog: &dyn RuleSourceCatalog,
709    action: &Statement,
710) -> Result<(), SQLError> {
711    validate_rule_action_namespace(catalog, action)?;
712    match action {
713        Statement::Select(select) => validate_rule_select_scopes(catalog, select),
714        Statement::Insert(insert) => {
715            validate_rule_ctes(catalog, &insert.with)?;
716            for expr in insert.rows.iter().flatten() {
717                validate_rule_expr_scopes(catalog, expr)?;
718            }
719            if let Some(select) = &insert.select_source {
720                validate_rule_select_scopes(catalog, select)?;
721            }
722            if let Some(conflict) = &insert.on_conflict {
723                for expression in &conflict.expressions {
724                    validate_rule_expr_scopes(catalog, expression)?;
725                }
726                if let Some(predicate) = conflict.predicate.as_deref() {
727                    validate_rule_expr_scopes(catalog, predicate)?;
728                }
729                if let OnConflictAction::Update {
730                    assignments,
731                    r#where,
732                } = &conflict.action
733                {
734                    let reference = assignments
735                        .iter()
736                        .find_map(|(_, expr)| {
737                            let mut shadowed = std::collections::BTreeSet::new();
738                            shadowed.insert(insert.target_qualifier.to_ascii_lowercase());
739                            first_rule_row_reference_in_expr(expr, &shadowed)
740                        })
741                        .or_else(|| {
742                            r#where.as_ref().and_then(|expr| {
743                                let mut shadowed = std::collections::BTreeSet::new();
744                                shadowed.insert(insert.target_qualifier.to_ascii_lowercase());
745                                first_rule_row_reference_in_expr(expr, &shadowed)
746                            })
747                        });
748                    if let Some(qualifier) = reference {
749                        return Err(invalid_rule_action_reference(&qualifier));
750                    }
751                    for (_, expr) in assignments {
752                        validate_rule_expr_scopes(catalog, expr)?;
753                    }
754                    if let Some(r#where) = r#where {
755                        validate_rule_expr_scopes(catalog, r#where)?;
756                    }
757                }
758            }
759            for projection in &insert.returning {
760                validate_rule_expr_scopes(catalog, &projection.expr)?;
761            }
762            Ok(())
763        }
764        Statement::Update(update) => {
765            validate_rule_ctes(catalog, &update.with)?;
766            if let Some(from) = &update.from {
767                validate_rule_from_scopes(catalog, from)?;
768            }
769            for expr in update
770                .assignments
771                .iter()
772                .map(|(_, expr)| expr)
773                .chain(update.r#where.iter())
774                .chain(update.returning.iter().map(|projection| &projection.expr))
775            {
776                validate_rule_expr_scopes(catalog, expr)?;
777            }
778            Ok(())
779        }
780        Statement::Delete(delete) => {
781            validate_rule_ctes(catalog, &delete.with)?;
782            if let Some(using) = &delete.using {
783                validate_rule_from_scopes(catalog, using)?;
784            }
785            for expr in delete
786                .r#where
787                .iter()
788                .chain(delete.returning.iter().map(|projection| &projection.expr))
789            {
790                validate_rule_expr_scopes(catalog, expr)?;
791            }
792            Ok(())
793        }
794        _ => Ok(()),
795    }
796}
797
798pub fn validate_trigger_transition_relation(
799    definition: &CreateTrigger,
800    hierarchy: &TableHierarchy,
801    transition: &TriggerTransitionRelation,
802) -> Result<(), SQLError> {
803    if !transition.is_table {
804        return Err(SQLError::Routine {
805            sqlstate: "0A000".into(),
806            message: "ROW variable naming in the REFERENCING clause is not supported".into(),
807        });
808    }
809    if definition.row && !hierarchy.parents.is_empty() {
810        return Err(SQLError::Routine {
811            sqlstate: "0A000".into(),
812            message: if hierarchy.partition_bound.is_some() {
813                "ROW triggers with transition tables are not supported on partitions".into()
814            } else {
815                "ROW triggers with transition tables are not supported on inheritance children"
816                    .into()
817            },
818        });
819    }
820    if definition.timing != TriggerTiming::After {
821        return Err(SQLError::Routine {
822            sqlstate: "42P17".into(),
823            message: "transition table name can only be specified for an AFTER trigger".into(),
824        });
825    }
826    if definition.events.contains(&TriggerEvent::Truncate) {
827        return Err(SQLError::Routine {
828            sqlstate: "0A000".into(),
829            message: "TRUNCATE triggers with transition tables are not supported".into(),
830        });
831    }
832    let mutation_events = definition
833        .events
834        .iter()
835        .filter(|event| {
836            matches!(
837                event,
838                TriggerEvent::Insert | TriggerEvent::Update | TriggerEvent::Delete
839            )
840        })
841        .count();
842    if mutation_events != 1 {
843        return Err(SQLError::Routine {
844            sqlstate: "0A000".into(),
845            message: "transition tables cannot be specified for triggers with more than one event"
846                .into(),
847        });
848    }
849    if !definition.update_columns.is_empty() {
850        return Err(SQLError::Routine {
851            sqlstate: "0A000".into(),
852            message: "transition tables cannot be specified for triggers with column lists".into(),
853        });
854    }
855    let valid_event = definition.events.iter().any(|event| {
856        if transition.is_new {
857            matches!(event, TriggerEvent::Insert | TriggerEvent::Update)
858        } else {
859            matches!(event, TriggerEvent::Delete | TriggerEvent::Update)
860        }
861    });
862    if !valid_event {
863        return Err(SQLError::Routine {
864            sqlstate: "42P17".into(),
865            message: format!(
866                "{} TABLE can only be specified for {} trigger",
867                if transition.is_new { "NEW" } else { "OLD" },
868                if transition.is_new {
869                    "an INSERT or UPDATE"
870                } else {
871                    "a DELETE or UPDATE"
872                }
873            ),
874        });
875    }
876    Ok(())
877}
878
879#[cfg(test)]
880mod tests;