Skip to main content

uqa_sql/catalog/events/definition/
rules.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7use super::EventAnalysisContext;
8use crate::{
9    ast::{ColumnType, CreateRule, Expr, RuleEvent, Statement},
10    catalog::{
11        events::{
12            validation::{
13                first_invalid_rule_condition_qualifier, is_boolean_type, rule_action_has_returning,
14                validate_rule_action_reference_scopes, validate_rule_returning_shape,
15                RuleConditionNameResolver, RuleRowTypeResolver,
16            },
17            RuleConditionBinding, RuleDependencies,
18        },
19        resolution::{RelationLookupMode, RelationResolution},
20        view::StoredViewKind,
21    },
22    plpgsql::bind_expr,
23    semantics::rules::action_binding::rule_action_has_set_operation,
24    SQLError,
25};
26use uqa_core::{RelationIdentity, Value};
27
28#[cfg(test)]
29mod tests;
30
31fn rule_condition_has_subquery(condition: &Expr) -> bool {
32    condition.any_node(&|node| {
33        matches!(
34            node,
35            Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. }
36        )
37    })
38}
39
40fn validate_rule_action_contract(definition: &CreateRule) -> Result<(), SQLError> {
41    if definition.condition.is_some()
42        && definition.actions.iter().any(rule_action_has_set_operation)
43    {
44        return Err(SQLError::Routine {
45            sqlstate: "0A000".into(),
46            message: "conditional UNION/INTERSECT/EXCEPT statements are not implemented".into(),
47        });
48    }
49    let returning_actions = definition
50        .actions
51        .iter()
52        .filter(|action| rule_action_has_returning(action))
53        .count();
54    if returning_actions > 1 {
55        return Err(SQLError::Routine {
56            sqlstate: "0A000".into(),
57            message: "cannot have multiple RETURNING lists in a rule".into(),
58        });
59    }
60    if returning_actions != 0 && definition.condition.is_some() {
61        return Err(SQLError::Routine {
62            sqlstate: "0A000".into(),
63            message: "RETURNING lists are not supported in conditional rules".into(),
64        });
65    }
66    if returning_actions != 0 && !definition.instead {
67        return Err(SQLError::Routine {
68            sqlstate: "0A000".into(),
69            message: "RETURNING lists are not supported in non-INSTEAD rules".into(),
70        });
71    }
72    Ok(())
73}
74
75impl EventAnalysisContext<'_> {
76    fn resolve_rule_event_relation_kind(
77        &self,
78        name: &str,
79        lookup_mode: RelationLookupMode,
80    ) -> Result<(RelationIdentity, &'static str), SQLError> {
81        let resolution = match lookup_mode {
82            RelationLookupMode::Dynamic => self.relations.resolve_visible_relation_kind(name)?,
83            RelationLookupMode::Bound => self.relations.resolve_bound_relation_kind(name)?,
84        };
85        if let RelationResolution::Found(canonical, "foreign table") = &resolution {
86            let relation = RelationIdentity::from_legacy_name(canonical).map_err(|error| {
87                SQLError::Internal(format!(
88                    "decode resolved rule relation `{canonical}`: {error}"
89                ))
90            })?;
91            return Err(SQLError::Routine {
92                sqlstate: "42809".into(),
93                message: format!("\"{}\" is a foreign table", relation.name),
94            });
95        }
96        Self::event_relation_from_resolution(name, resolution)
97    }
98
99    fn resolve_visible_rule_action_relation(
100        &self,
101        name: &str,
102    ) -> Result<RelationIdentity, SQLError> {
103        let Some((canonical, kind)) = self
104            .relations
105            .resolve_visible_relation_kind(name)?
106            .into_found()
107        else {
108            return Err(SQLError::UnknownTable(name.to_string()));
109        };
110        if !matches!(kind, "table" | "view" | "materialized view") {
111            return Err(SQLError::UnknownTable(name.to_string()));
112        }
113        RelationIdentity::from_legacy_name(&canonical).map_err(|error| {
114            SQLError::Internal(format!(
115                "decode resolved rule action relation `{canonical}`: {error}"
116            ))
117        })
118    }
119
120    pub fn resolve_rule_relation(&self, name: &str) -> Result<RelationIdentity, SQLError> {
121        let RelationResolution::Found(canonical, kind) =
122            self.relations.resolve_bound_relation_kind(name)?
123        else {
124            return Err(SQLError::UnknownTable(name.to_string()));
125        };
126        if !matches!(kind, "table" | "view" | "materialized view") {
127            return Err(SQLError::UnknownTable(name.to_string()));
128        }
129        RelationIdentity::from_legacy_name(&canonical).map_err(|error| {
130            SQLError::Internal(format!("decode bound rule relation `{canonical}`: {error}"))
131        })
132    }
133
134    pub fn rule_relation_columns(&self, name: &str) -> Result<Vec<(String, ColumnType)>, SQLError> {
135        Ok(self
136            .rule_relation_definitions(name)?
137            .into_iter()
138            .map(|column| (column.name, column.ty))
139            .collect())
140    }
141
142    pub fn rule_relation_definitions(
143        &self,
144        name: &str,
145    ) -> Result<Vec<crate::ast::ColumnDef>, SQLError> {
146        if let Some(columns) = self
147            .returning
148            .catalog
149            .try_describe_table_row_type(name)
150            .map_err(|error| SQLError::Internal(format!("read rule columns: {error}")))?
151        {
152            return Ok(columns);
153        }
154        let relation = RelationIdentity::from_legacy_name(name).map_err(|error| {
155            SQLError::Internal(format!("decode rule relation `{name}`: {error}"))
156        })?;
157        let view = self
158            .catalog
159            .view(&relation)
160            .ok_or_else(|| SQLError::UnknownTable(name.to_string()))?;
161        let schema = self.catalog.stored_view_schema(&view)?;
162        Ok(schema
163            .columns()
164            .iter()
165            .enumerate()
166            .map(|(index, name)| {
167                let mut column = crate::ast::ColumnDef::nullable(
168                    schema.public_name(index).unwrap_or(name),
169                    schema
170                        .column_type(index)
171                        .cloned()
172                        .unwrap_or(ColumnType::Text),
173                );
174                column.collation_oid = schema.column_collation(index);
175                column
176            })
177            .collect())
178    }
179
180    fn validate_select_rule_contract(
181        definition: &CreateRule,
182        is_view: bool,
183    ) -> Result<(), SQLError> {
184        if definition.event != RuleEvent::Select && definition.name == "_RETURN" {
185            let relation =
186                RelationIdentity::from_legacy_name(&definition.table).map_err(|error| {
187                    SQLError::Internal(format!(
188                        "decode rule relation `{}`: {error}",
189                        definition.table
190                    ))
191                })?;
192            return Err(SQLError::Routine {
193                sqlstate: "42P17".into(),
194                message: format!(
195                    "non-view rule for \"{}\" must not be named \"_RETURN\"",
196                    relation.name
197                ),
198            });
199        }
200        if definition.event == RuleEvent::Select {
201            if !is_view {
202                return Err(SQLError::Routine {
203                    sqlstate: "42809".into(),
204                    message: format!(
205                        "relation \"{}\" cannot have ON SELECT rules",
206                        definition.table
207                    ),
208                });
209            }
210            if definition.name != "_RETURN"
211                || !definition.instead
212                || definition.condition.is_some()
213                || !matches!(definition.actions.as_slice(), [Statement::Select(_)])
214            {
215                return Err(SQLError::Routine {
216                    sqlstate: "42P17".into(),
217                    message: "view rule must be named \"_RETURN\", unconditional, INSTEAD, and have one SELECT action".into(),
218                });
219            }
220        }
221        Ok(())
222    }
223
224    fn validate_rule_condition(
225        &self,
226        condition: &mut Expr,
227        columns: &[crate::ast::ColumnDef],
228        event: RuleEvent,
229        stored_plan: Option<&crate::plan::ExpressionPlan>,
230        stored_binding: Option<&RuleConditionBinding>,
231    ) -> Result<Option<(crate::plan::ExpressionPlan, RuleConditionBinding)>, SQLError> {
232        let has_subquery = rule_condition_has_subquery(condition);
233        if condition.contains_aggregate()
234            || condition.contains_window()
235            || condition.any_node(&|node| {
236                matches!(node, Expr::Func { name, .. } if self.routines.has_registered_aggregate_function(name))
237            })
238        {
239            return Err(SQLError::Routine {
240                sqlstate: "42803".into(),
241                message: "aggregate and window functions are not allowed in rule WHERE conditions"
242                    .into(),
243            });
244        }
245        if !has_subquery && condition.any_node(&|node| matches!(node, Expr::Column(_))) {
246            *condition = bind_expr(condition, &mut RuleConditionNameResolver { columns, event })?;
247        }
248        if let Some(reference) = first_invalid_rule_condition_qualifier(condition) {
249            return Err(SQLError::Routine {
250                sqlstate: "42P10".into(),
251                message: format!("rule WHERE condition cannot refer to relation \"{reference}\""),
252            });
253        }
254        if !has_subquery {
255            let bound = bind_expr(
256                condition,
257                &mut RuleRowTypeResolver::for_condition(columns, event),
258            )?;
259            let lowered = crate::plan::ExpressionPlan::lower(bound);
260            match crate::common_context_expression_type(
261                &lowered.scalar,
262                &crate::RowSchema::default(),
263                &[],
264                Some(self.routines),
265            )? {
266                Some(ty) if !is_boolean_type(&ty) => {
267                    return Err(SQLError::TypeMismatch(format!(
268                        "argument of WHERE must be type boolean, not type {}",
269                        ty.sql_name()
270                    )))
271                }
272                None => {
273                    if let Expr::Literal(value @ (Value::Str(_) | Value::FixedChar(_))) = condition
274                    {
275                        *value = crate::expr::cast_value(value, "boolean")?;
276                    } else {
277                        *condition = Expr::Cast {
278                            implicit: true,
279                            expr: Box::new(condition.clone()),
280                            ty: "boolean".into(),
281                        };
282                    }
283                }
284                Some(_) => {}
285            }
286        }
287        let (mut plan, binding, reused) = if let Some((plan, binding)) =
288            stored_plan.zip(stored_binding)
289        {
290            let mut plan = plan.clone();
291            let binding = binding.reallocate_plan_relations(&mut plan);
292            (plan, binding, true)
293        } else {
294            let plan = crate::plan::ExpressionPlan::lower_with(condition.clone(), &|name: &str| {
295                self.routines.has_registered_aggregate_function(name)
296            });
297            let column_names = columns
298                .iter()
299                .map(|column| column.name.clone())
300                .collect::<Vec<_>>();
301            (
302                plan,
303                RuleConditionBinding::for_event(&column_names, event),
304                false,
305            )
306        };
307        if !reused {
308            for subquery in &mut plan.subqueries {
309                self.bind_rule_condition_subquery_relations(subquery)?;
310            }
311        }
312        let schema = binding.row_schema(columns);
313        let ty = self
314            .stored_routines
315            .bind_rule_condition(&mut plan, &schema, event)?;
316        if let Some(ty) = ty {
317            if !is_boolean_type(&ty) {
318                return Err(SQLError::TypeMismatch(format!(
319                    "argument of WHERE must be type boolean, not type {}",
320                    ty.sql_name()
321                )));
322            }
323        }
324        crate::catalog::regrole_dependencies::reject_stored_regrole_constants_with(
325            self.regroles,
326            condition,
327            None,
328        )?;
329        Ok(Some((plan, binding)))
330    }
331
332    fn bind_rule_condition_subquery_relations(
333        &self,
334        subquery: &mut crate::plan::QueryPlan,
335    ) -> Result<(), SQLError> {
336        let namespace = self.namespaces.stored_query_namespace();
337        crate::binding::stored_relations::bind_stored_query_relations(
338            &crate::binding::stored_relations::StoredQueryBindingContext {
339                relations: self.relations,
340                lookup_mode: RelationLookupMode::Dynamic,
341                sequences: self.sequences,
342                temporary_schema: &namespace.temporary_schema,
343                transition_relations: &namespace.transition_relations,
344            },
345            subquery,
346            "CREATE RULE",
347            false,
348            false,
349        )?;
350        Ok(())
351    }
352
353    fn canonicalize_rule_action_target(
354        &self,
355        action: &mut Statement,
356        lookup_mode: RelationLookupMode,
357    ) -> Result<(), SQLError> {
358        let (target, target_relation_bound) = match action {
359            Statement::Insert(statement) => {
360                (&mut statement.table, &mut statement.target_relation_bound)
361            }
362            Statement::Update(statement) => {
363                (&mut statement.table, &mut statement.target_relation_bound)
364            }
365            Statement::Delete(statement) => {
366                (&mut statement.table, &mut statement.target_relation_bound)
367            }
368            _ => return Ok(()),
369        };
370        let relation = if lookup_mode == RelationLookupMode::Bound || *target_relation_bound {
371            self.resolve_rule_relation(target)?
372        } else {
373            self.resolve_visible_rule_action_relation(target)?
374        };
375        *target_relation_bound = true;
376        *target = relation.qualified_name();
377        Ok(())
378    }
379
380    pub fn rule_action_target_columns(
381        &self,
382        action: &Statement,
383    ) -> Result<std::collections::BTreeSet<String>, SQLError> {
384        crate::semantics::rules::action_binding::rule_action_target_columns(self.sources, action)
385    }
386
387    fn rule_action_target_row_type(
388        &self,
389        action: &Statement,
390    ) -> Result<Vec<(String, ColumnType)>, SQLError> {
391        crate::semantics::rules::action_binding::rule_action_target_row_type(self.sources, action)
392    }
393
394    fn validate_rule_action_definition(
395        &self,
396        action: &mut Statement,
397        event_columns: &[crate::ast::ColumnDef],
398        event: RuleEvent,
399        lookup_mode: RelationLookupMode,
400    ) -> Result<RuleDependencies, SQLError> {
401        self.canonicalize_rule_action_target(action, lookup_mode)?;
402        let mut dependencies =
403            crate::binding::stored_relations::bind_rule_action_relation_dependencies(
404                self.relations,
405                action,
406                lookup_mode,
407            )?;
408        let event_row_type = event_columns
409            .iter()
410            .map(|column| (column.name.clone(), column.ty.clone()))
411            .collect::<Vec<_>>();
412        let action_row_type = self.rule_action_target_row_type(action)?;
413        let action_columns: std::collections::BTreeSet<String> = action_row_type
414            .iter()
415            .map(|(column, _)| column.clone())
416            .collect();
417        if lookup_mode == RelationLookupMode::Dynamic {
418            *action = crate::semantics::rules::action_binding::expand_rule_action_row_stars(
419                self.sources,
420                action,
421                &action_columns,
422                &event_row_type,
423                event,
424            )?;
425            *action = crate::semantics::rules::action_binding::expand_rule_action_returning_stars(
426                action,
427                &action_row_type,
428            );
429        }
430        dependencies.columns.extend(
431            crate::binding::stored_columns::bind_rule_action_column_dependencies(
432                self.columns,
433                action,
434            )?,
435        );
436        validate_rule_action_reference_scopes(self.sources, action)?;
437        let mut row_resolver = RuleRowTypeResolver::for_action(event_columns, event);
438        let bound = crate::semantics::rules::action_binding::bind_rule_action(
439            self.sources,
440            action,
441            &action_columns,
442            &mut row_resolver,
443        )?;
444        let schema = crate::semantics::returning::dml_statement_returning_schema(
445            self.returning,
446            bound.clone(),
447        )?;
448        let mut stored_plan = crate::plan::UnifiedPlan::lower_with(bound, &|name: &str| {
449            self.routines.has_registered_aggregate_function(name)
450        });
451        crate::catalog::regrole_dependencies::reject_stored_plan_regrole_constants_with(
452            self.regroles,
453            &mut stored_plan,
454        )?;
455        let bound_routines = self.stored_routines.bind_statement(&stored_plan)?;
456        row_resolver.validate_event_references()?;
457        if let Some(routine_plan) = &bound_routines.query {
458            crate::catalog::events::dependencies::collect_query_routine_dependencies(
459                routine_plan,
460                &mut dependencies,
461            );
462        }
463        crate::catalog::stored_ast::bind_stored_statement_sites(action, &bound_routines.sites)?;
464        if let Some(schema) = schema {
465            validate_rule_returning_shape(&schema, &event_row_type)?;
466        }
467        Ok(dependencies)
468    }
469
470    fn bind_rule_condition_object_dependencies(
471        &self,
472        condition: &mut Expr,
473        condition_plan: Option<&crate::plan::ExpressionPlan>,
474        columns: &[crate::ast::ColumnDef],
475        event: RuleEvent,
476        dependencies: &mut RuleDependencies,
477    ) -> Result<(), SQLError> {
478        if let Some(plan) = condition_plan {
479            // Every analyzed constant and coercion belongs to the stored definition, including implicit enum inputs.
480            let lowered =
481                crate::plan::ExpressionPlan::lower_with(condition.clone(), &|name: &str| {
482                    self.routines.has_registered_aggregate_function(name)
483                });
484            let sites = crate::binding::syntax_sites::expression_syntax_sites(&lowered, plan)?;
485            crate::catalog::stored_ast::bind_stored_expression_sites(condition, &sites)?;
486            crate::catalog::events::dependencies::collect_expression_routine_dependencies(
487                plan,
488                dependencies,
489            );
490            for subquery in &plan.subqueries {
491                crate::catalog::events::dependencies::collect_query_relation_dependencies(
492                    subquery,
493                    dependencies,
494                    &std::collections::BTreeSet::new(),
495                )?;
496            }
497            let routine_references =
498                crate::binding::stored_routines::collect_expression_routine_references(plan)?;
499            crate::catalog::stored_ast::bind_stored_expression_routines(
500                condition,
501                &routine_references,
502            )?;
503            return Ok(());
504        }
505
506        let bound = bind_expr(
507            condition,
508            &mut RuleRowTypeResolver::for_condition(columns, event),
509        )?;
510        let lowered = crate::plan::ExpressionPlan::lower_with(bound, &|name: &str| {
511            self.routines.has_registered_aggregate_function(name)
512        });
513        let mut dependency_plan = lowered.clone();
514        self.stored_routines.bind_expression(
515            &mut dependency_plan,
516            &[],
517            &crate::RowSchema::default(),
518        )?;
519        crate::catalog::events::dependencies::collect_expression_routine_dependencies(
520            &dependency_plan,
521            dependencies,
522        );
523        let sites =
524            crate::binding::syntax_sites::expression_syntax_sites(&lowered, &dependency_plan)?;
525        crate::catalog::stored_ast::bind_stored_expression_sites(condition, &sites).map(|_| ())
526    }
527
528    pub fn validate_rule_definition(
529        &self,
530        definition: &mut CreateRule,
531        lookup_mode: RelationLookupMode,
532        stored_condition_plan: Option<&crate::plan::ExpressionPlan>,
533        stored_condition_binding: Option<&RuleConditionBinding>,
534    ) -> Result<
535        (
536            RelationIdentity,
537            Option<crate::plan::ExpressionPlan>,
538            Option<RuleConditionBinding>,
539            RuleDependencies,
540        ),
541        SQLError,
542    > {
543        let (relation, _) =
544            self.resolve_rule_event_relation_kind(&definition.table, lookup_mode)?;
545        definition.table = relation.qualified_name();
546        if lookup_mode == RelationLookupMode::Dynamic {
547            self.ensure_event_relation_owner(&relation, None)?;
548        }
549        let stored_view_kind = self.catalog.view_kind(&relation);
550        if stored_view_kind == Some(StoredViewKind::Materialized) {
551            return Err(SQLError::Routine {
552                sqlstate: "0A000".into(),
553                message: "rules on materialized views are not supported".into(),
554            });
555        }
556        let is_view = stored_view_kind == Some(StoredViewKind::View);
557        Self::validate_select_rule_contract(definition, is_view)?;
558        let columns = self.rule_relation_definitions(&definition.table)?;
559        let mut dependencies = RuleDependencies::default();
560        if let Some(condition) = &mut definition.condition {
561            let condition_dependencies =
562                crate::binding::stored_relations::bind_rule_condition_relation_dependencies(
563                    self.relations,
564                    condition,
565                    lookup_mode,
566                )?;
567            dependencies
568                .relations
569                .extend(condition_dependencies.relations);
570            dependencies.columns.extend(
571                crate::binding::stored_columns::bind_rule_condition_column_dependencies(
572                    self.columns,
573                    condition,
574                )?,
575            );
576        }
577        let condition = definition
578            .condition
579            .as_mut()
580            .map(|condition| {
581                self.validate_rule_condition(
582                    condition,
583                    &columns,
584                    definition.event,
585                    stored_condition_plan,
586                    stored_condition_binding,
587                )
588            })
589            .transpose()?
590            .flatten();
591        let (condition_plan, condition_binding) = condition.map_or_else(
592            || (None, None),
593            |(plan, binding)| (Some(plan), Some(binding)),
594        );
595        if let Some(condition) = &mut definition.condition {
596            self.bind_rule_condition_object_dependencies(
597                condition,
598                condition_plan.as_ref(),
599                &columns,
600                definition.event,
601                &mut dependencies,
602            )?;
603            dependencies.columns.extend(
604                crate::semantics::rules::action_binding::rule_expr_row_columns(condition)
605                    .into_iter()
606                    .map(|column| crate::catalog::events::RuleColumnDependency {
607                        relation: relation.clone(),
608                        column,
609                    }),
610            );
611        }
612        validate_rule_action_contract(definition)?;
613        for action in &mut definition.actions {
614            let action_dependencies = self.validate_rule_action_definition(
615                action,
616                &columns,
617                definition.event,
618                lookup_mode,
619            )?;
620            dependencies.relations.extend(action_dependencies.relations);
621            dependencies.columns.extend(action_dependencies.columns);
622            dependencies.routines.extend(action_dependencies.routines);
623            let action_columns = self.rule_action_target_columns(action)?;
624            dependencies.columns.extend(
625                crate::semantics::rules::action_binding::rule_statement_row_columns(
626                    self.sources,
627                    action,
628                    &action_columns,
629                )?
630                .into_iter()
631                .map(|column| crate::catalog::events::RuleColumnDependency {
632                    relation: relation.clone(),
633                    column,
634                }),
635            );
636        }
637        Ok((relation, condition_plan, condition_binding, dependencies))
638    }
639}