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