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        if let Some(columns) = self
136            .returning
137            .catalog
138            .try_describe_table_row_type(name)
139            .map_err(|error| SQLError::Internal(format!("read rule columns: {error}")))?
140        {
141            return Ok(columns
142                .into_iter()
143                .map(|column| (column.name, column.ty))
144                .collect());
145        }
146        let relation = RelationIdentity::from_legacy_name(name).map_err(|error| {
147            SQLError::Internal(format!("decode rule relation `{name}`: {error}"))
148        })?;
149        let view = self
150            .catalog
151            .view(&relation)
152            .ok_or_else(|| SQLError::UnknownTable(name.to_string()))?;
153        let schema = self.catalog.stored_view_schema(&view)?;
154        Ok(schema
155            .columns()
156            .iter()
157            .enumerate()
158            .map(|(index, name)| {
159                (
160                    schema.public_name(index).unwrap_or(name).to_string(),
161                    schema
162                        .column_type(index)
163                        .cloned()
164                        .unwrap_or(ColumnType::Text),
165                )
166            })
167            .collect())
168    }
169
170    fn validate_select_rule_contract(
171        definition: &CreateRule,
172        is_view: bool,
173    ) -> Result<(), SQLError> {
174        if definition.event != RuleEvent::Select && definition.name == "_RETURN" {
175            let relation =
176                RelationIdentity::from_legacy_name(&definition.table).map_err(|error| {
177                    SQLError::Internal(format!(
178                        "decode rule relation `{}`: {error}",
179                        definition.table
180                    ))
181                })?;
182            return Err(SQLError::Routine {
183                sqlstate: "42P17".into(),
184                message: format!(
185                    "non-view rule for \"{}\" must not be named \"_RETURN\"",
186                    relation.name
187                ),
188            });
189        }
190        if definition.event == RuleEvent::Select {
191            if !is_view {
192                return Err(SQLError::Routine {
193                    sqlstate: "42809".into(),
194                    message: format!(
195                        "relation \"{}\" cannot have ON SELECT rules",
196                        definition.table
197                    ),
198                });
199            }
200            if definition.name != "_RETURN"
201                || !definition.instead
202                || definition.condition.is_some()
203                || !matches!(definition.actions.as_slice(), [Statement::Select(_)])
204            {
205                return Err(SQLError::Routine {
206                    sqlstate: "42P17".into(),
207                    message: "view rule must be named \"_RETURN\", unconditional, INSTEAD, and have one SELECT action".into(),
208                });
209            }
210        }
211        Ok(())
212    }
213
214    fn validate_rule_condition(
215        &self,
216        condition: &mut Expr,
217        columns: &[(String, ColumnType)],
218        event: RuleEvent,
219        stored_plan: Option<&crate::plan::ExpressionPlan>,
220        stored_binding: Option<&RuleConditionBinding>,
221    ) -> Result<Option<(crate::plan::ExpressionPlan, RuleConditionBinding)>, SQLError> {
222        let has_subquery = rule_condition_has_subquery(condition);
223        if condition.contains_aggregate()
224            || condition.contains_window()
225            || condition.any_node(&|node| {
226                matches!(node, Expr::Func { name, .. } if self.routines.has_registered_aggregate_function(name))
227            })
228        {
229            return Err(SQLError::Routine {
230                sqlstate: "42803".into(),
231                message: "aggregate and window functions are not allowed in rule WHERE conditions"
232                    .into(),
233            });
234        }
235        if !has_subquery && condition.any_node(&|node| matches!(node, Expr::Column(_))) {
236            *condition = bind_expr(condition, &mut RuleConditionNameResolver { columns, event })?;
237        }
238        if let Some(reference) = first_invalid_rule_condition_qualifier(condition) {
239            return Err(SQLError::Routine {
240                sqlstate: "42P10".into(),
241                message: format!("rule WHERE condition cannot refer to relation \"{reference}\""),
242            });
243        }
244        if !has_subquery {
245            let bound = bind_expr(condition, &mut RuleRowTypeResolver { columns, event })?;
246            let lowered = crate::plan::ExpressionPlan::lower(bound);
247            match crate::common_context_expression_type(
248                &lowered.scalar,
249                &crate::RowSchema::default(),
250                &[],
251                Some(self.routines),
252            )? {
253                Some(ty) if !is_boolean_type(&ty) => {
254                    return Err(SQLError::TypeMismatch(format!(
255                        "argument of WHERE must be type boolean, not type {}",
256                        ty.sql_name()
257                    )))
258                }
259                None => {
260                    if let Expr::Literal(value @ (Value::Str(_) | Value::FixedChar(_))) = condition
261                    {
262                        *value = crate::expr::cast_value(value, "boolean")?;
263                    } else {
264                        *condition = Expr::Cast {
265                            implicit: true,
266                            expr: Box::new(condition.clone()),
267                            ty: "boolean".into(),
268                        };
269                    }
270                }
271                Some(_) => {}
272            }
273        }
274        let (mut plan, binding, reused) = if let Some((plan, binding)) =
275            stored_plan.zip(stored_binding)
276        {
277            let mut plan = plan.clone();
278            let binding = binding.reallocate_plan_relations(&mut plan);
279            (plan, binding, true)
280        } else {
281            let plan = crate::plan::ExpressionPlan::lower_with(condition.clone(), &|name: &str| {
282                self.routines.has_registered_aggregate_function(name)
283            });
284            let column_names = columns
285                .iter()
286                .map(|(name, _)| name.clone())
287                .collect::<Vec<_>>();
288            (
289                plan,
290                RuleConditionBinding::for_event(&column_names, event),
291                false,
292            )
293        };
294        if !reused {
295            for subquery in &mut plan.subqueries {
296                self.bind_rule_condition_subquery_relations(subquery)?;
297            }
298        }
299        let schema = binding.row_schema(columns);
300        let ty = self
301            .stored_routines
302            .bind_expression(&mut plan, &[], &schema)?;
303        if let Some(ty) = ty {
304            if !is_boolean_type(&ty) {
305                return Err(SQLError::TypeMismatch(format!(
306                    "argument of WHERE must be type boolean, not type {}",
307                    ty.sql_name()
308                )));
309            }
310        }
311        crate::catalog::regrole_dependencies::reject_stored_regrole_constants_with(
312            self.regroles,
313            condition,
314            None,
315        )?;
316        Ok(Some((plan, binding)))
317    }
318
319    fn bind_rule_condition_subquery_relations(
320        &self,
321        subquery: &mut crate::plan::QueryPlan,
322    ) -> Result<(), SQLError> {
323        let namespace = self.namespaces.stored_query_namespace();
324        crate::binding::stored_relations::bind_stored_query_relations(
325            &crate::binding::stored_relations::StoredQueryBindingContext {
326                relations: self.relations,
327                lookup_mode: RelationLookupMode::Dynamic,
328                sequences: self.sequences,
329                temporary_schema: &namespace.temporary_schema,
330                transition_relations: &namespace.transition_relations,
331            },
332            subquery,
333            "CREATE RULE",
334            false,
335            false,
336        )?;
337        Ok(())
338    }
339
340    fn canonicalize_rule_action_target(
341        &self,
342        action: &mut Statement,
343        lookup_mode: RelationLookupMode,
344    ) -> Result<(), SQLError> {
345        let (target, target_relation_bound) = match action {
346            Statement::Insert(statement) => {
347                (&mut statement.table, &mut statement.target_relation_bound)
348            }
349            Statement::Update(statement) => {
350                (&mut statement.table, &mut statement.target_relation_bound)
351            }
352            Statement::Delete(statement) => {
353                (&mut statement.table, &mut statement.target_relation_bound)
354            }
355            _ => return Ok(()),
356        };
357        let relation = if lookup_mode == RelationLookupMode::Bound || *target_relation_bound {
358            self.resolve_rule_relation(target)?
359        } else {
360            self.resolve_visible_rule_action_relation(target)?
361        };
362        *target_relation_bound = true;
363        *target = relation.qualified_name();
364        Ok(())
365    }
366
367    pub fn rule_action_target_columns(
368        &self,
369        action: &Statement,
370    ) -> Result<std::collections::BTreeSet<String>, SQLError> {
371        crate::semantics::rules::action_binding::rule_action_target_columns(self.sources, action)
372    }
373
374    fn rule_action_target_row_type(
375        &self,
376        action: &Statement,
377    ) -> Result<Vec<(String, ColumnType)>, SQLError> {
378        crate::semantics::rules::action_binding::rule_action_target_row_type(self.sources, action)
379    }
380
381    fn validate_rule_action_definition(
382        &self,
383        action: &mut Statement,
384        event_columns: &[(String, ColumnType)],
385        event: RuleEvent,
386        lookup_mode: RelationLookupMode,
387    ) -> Result<RuleDependencies, SQLError> {
388        self.canonicalize_rule_action_target(action, lookup_mode)?;
389        let mut dependencies =
390            crate::binding::stored_relations::bind_rule_action_relation_dependencies(
391                self.relations,
392                action,
393                lookup_mode,
394            )?;
395        let action_row_type = self.rule_action_target_row_type(action)?;
396        let action_columns: std::collections::BTreeSet<String> = action_row_type
397            .iter()
398            .map(|(column, _)| column.clone())
399            .collect();
400        if lookup_mode == RelationLookupMode::Dynamic {
401            *action = crate::semantics::rules::action_binding::expand_rule_action_row_stars(
402                self.sources,
403                action,
404                &action_columns,
405                event_columns,
406                event,
407            )?;
408            *action = crate::semantics::rules::action_binding::expand_rule_action_returning_stars(
409                action,
410                &action_row_type,
411            );
412        }
413        dependencies.columns.extend(
414            crate::binding::stored_columns::bind_rule_action_column_dependencies(
415                self.columns,
416                action,
417            )?,
418        );
419        validate_rule_action_reference_scopes(self.sources, action)?;
420        let bound = crate::semantics::rules::action_binding::bind_rule_action(
421            self.sources,
422            action,
423            &action_columns,
424            &mut RuleRowTypeResolver {
425                columns: event_columns,
426                event,
427            },
428        )?;
429        let schema = crate::semantics::returning::dml_statement_returning_schema(
430            self.returning,
431            bound.clone(),
432        )?;
433        let mut stored_plan = crate::plan::UnifiedPlan::lower_with(bound, &|name: &str| {
434            self.routines.has_registered_aggregate_function(name)
435        });
436        crate::catalog::regrole_dependencies::reject_stored_plan_regrole_constants_with(
437            self.regroles,
438            &mut stored_plan,
439        )?;
440        let bound_routines = self.stored_routines.bind_statement(&stored_plan)?;
441        if let Some(routine_plan) = &bound_routines.query {
442            crate::catalog::events::dependencies::collect_query_routine_dependencies(
443                routine_plan,
444                &mut dependencies,
445            );
446        }
447        crate::catalog::stored_ast::bind_stored_statement_sites(action, &bound_routines.sites)?;
448        if let Some(schema) = schema {
449            validate_rule_returning_shape(&schema, event_columns)?;
450        }
451        Ok(dependencies)
452    }
453
454    fn bind_rule_condition_object_dependencies(
455        &self,
456        condition: &mut Expr,
457        condition_plan: Option<&crate::plan::ExpressionPlan>,
458        columns: &[(String, ColumnType)],
459        event: RuleEvent,
460        dependencies: &mut RuleDependencies,
461    ) -> Result<(), SQLError> {
462        if let Some(plan) = condition_plan {
463            // Every analyzed constant and coercion belongs to the stored definition, including implicit enum inputs.
464            let lowered =
465                crate::plan::ExpressionPlan::lower_with(condition.clone(), &|name: &str| {
466                    self.routines.has_registered_aggregate_function(name)
467                });
468            let sites = crate::binding::syntax_sites::expression_syntax_sites(&lowered, plan)?;
469            crate::catalog::stored_ast::bind_stored_expression_sites(condition, &sites)?;
470            crate::catalog::events::dependencies::collect_expression_routine_dependencies(
471                plan,
472                dependencies,
473            );
474            for subquery in &plan.subqueries {
475                crate::catalog::events::dependencies::collect_query_relation_dependencies(
476                    subquery,
477                    dependencies,
478                    &std::collections::BTreeSet::new(),
479                )?;
480            }
481            let routine_references =
482                crate::binding::stored_routines::collect_expression_routine_references(plan)?;
483            crate::catalog::stored_ast::bind_stored_expression_routines(
484                condition,
485                &routine_references,
486            )?;
487            return Ok(());
488        }
489
490        let bound = bind_expr(condition, &mut RuleRowTypeResolver { columns, event })?;
491        let lowered = crate::plan::ExpressionPlan::lower_with(bound, &|name: &str| {
492            self.routines.has_registered_aggregate_function(name)
493        });
494        let mut dependency_plan = lowered.clone();
495        self.stored_routines.bind_expression(
496            &mut dependency_plan,
497            &[],
498            &crate::RowSchema::default(),
499        )?;
500        crate::catalog::events::dependencies::collect_expression_routine_dependencies(
501            &dependency_plan,
502            dependencies,
503        );
504        let sites =
505            crate::binding::syntax_sites::expression_syntax_sites(&lowered, &dependency_plan)?;
506        crate::catalog::stored_ast::bind_stored_expression_sites(condition, &sites).map(|_| ())
507    }
508
509    pub fn validate_rule_definition(
510        &self,
511        definition: &mut CreateRule,
512        lookup_mode: RelationLookupMode,
513        stored_condition_plan: Option<&crate::plan::ExpressionPlan>,
514        stored_condition_binding: Option<&RuleConditionBinding>,
515    ) -> Result<
516        (
517            RelationIdentity,
518            Option<crate::plan::ExpressionPlan>,
519            Option<RuleConditionBinding>,
520            RuleDependencies,
521        ),
522        SQLError,
523    > {
524        let (relation, _) =
525            self.resolve_rule_event_relation_kind(&definition.table, lookup_mode)?;
526        definition.table = relation.qualified_name();
527        if lookup_mode == RelationLookupMode::Dynamic {
528            self.ensure_event_relation_owner(&relation, None)?;
529        }
530        let stored_view_kind = self.catalog.view_kind(&relation);
531        if stored_view_kind == Some(StoredViewKind::Materialized) {
532            return Err(SQLError::Routine {
533                sqlstate: "0A000".into(),
534                message: "rules on materialized views are not supported".into(),
535            });
536        }
537        let is_view = stored_view_kind == Some(StoredViewKind::View);
538        Self::validate_select_rule_contract(definition, is_view)?;
539        let columns = self.rule_relation_columns(&definition.table)?;
540        let mut dependencies = RuleDependencies::default();
541        if let Some(condition) = &mut definition.condition {
542            let condition_dependencies =
543                crate::binding::stored_relations::bind_rule_condition_relation_dependencies(
544                    self.relations,
545                    condition,
546                    lookup_mode,
547                )?;
548            dependencies
549                .relations
550                .extend(condition_dependencies.relations);
551            dependencies.columns.extend(
552                crate::binding::stored_columns::bind_rule_condition_column_dependencies(
553                    self.columns,
554                    condition,
555                )?,
556            );
557        }
558        let condition = definition
559            .condition
560            .as_mut()
561            .map(|condition| {
562                self.validate_rule_condition(
563                    condition,
564                    &columns,
565                    definition.event,
566                    stored_condition_plan,
567                    stored_condition_binding,
568                )
569            })
570            .transpose()?
571            .flatten();
572        let (condition_plan, condition_binding) = condition.map_or_else(
573            || (None, None),
574            |(plan, binding)| (Some(plan), Some(binding)),
575        );
576        if let Some(condition) = &mut definition.condition {
577            self.bind_rule_condition_object_dependencies(
578                condition,
579                condition_plan.as_ref(),
580                &columns,
581                definition.event,
582                &mut dependencies,
583            )?;
584            dependencies.columns.extend(
585                crate::semantics::rules::action_binding::rule_expr_row_columns(condition)
586                    .into_iter()
587                    .map(|column| crate::catalog::events::RuleColumnDependency {
588                        relation: relation.clone(),
589                        column,
590                    }),
591            );
592        }
593        validate_rule_action_contract(definition)?;
594        for action in &mut definition.actions {
595            let action_dependencies = self.validate_rule_action_definition(
596                action,
597                &columns,
598                definition.event,
599                lookup_mode,
600            )?;
601            dependencies.relations.extend(action_dependencies.relations);
602            dependencies.columns.extend(action_dependencies.columns);
603            dependencies.routines.extend(action_dependencies.routines);
604            let action_columns = self.rule_action_target_columns(action)?;
605            dependencies.columns.extend(
606                crate::semantics::rules::action_binding::rule_statement_row_columns(
607                    self.sources,
608                    action,
609                    &action_columns,
610                )?
611                .into_iter()
612                .map(|column| crate::catalog::events::RuleColumnDependency {
613                    relation: relation.clone(),
614                    column,
615                }),
616            );
617        }
618        Ok((relation, condition_plan, condition_binding, dependencies))
619    }
620}