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                        implicit: true,
333                        expr: Box::new(condition.clone()),
334                        ty: "boolean".into(),
335                    };
336                }
337            }
338            Some(_) => {}
339        }
340        crate::catalog::regrole_dependencies::reject_stored_regrole_constants_with(
341            self.regroles,
342            condition,
343            None,
344        )?;
345        Ok(None)
346    }
347
348    fn bind_rule_condition_subquery_relations(
349        &self,
350        subquery: &mut crate::plan::QueryPlan,
351    ) -> Result<(), SQLError> {
352        let namespace = self.namespaces.stored_query_namespace();
353        crate::binding::stored_relations::bind_stored_query_relations(
354            &crate::binding::stored_relations::StoredQueryBindingContext {
355                relations: self.relations,
356                lookup_mode: RelationLookupMode::Dynamic,
357                sequences: self.sequences,
358                temporary_schema: &namespace.temporary_schema,
359                transition_relations: &namespace.transition_relations,
360            },
361            subquery,
362            "CREATE RULE",
363            false,
364            false,
365        )?;
366        Ok(())
367    }
368
369    fn canonicalize_rule_action_target(
370        &self,
371        action: &mut Statement,
372        lookup_mode: RelationLookupMode,
373    ) -> Result<(), SQLError> {
374        let (target, target_relation_bound) = match action {
375            Statement::Insert(statement) => {
376                (&mut statement.table, &mut statement.target_relation_bound)
377            }
378            Statement::Update(statement) => {
379                (&mut statement.table, &mut statement.target_relation_bound)
380            }
381            Statement::Delete(statement) => {
382                (&mut statement.table, &mut statement.target_relation_bound)
383            }
384            _ => return Ok(()),
385        };
386        let relation = if lookup_mode == RelationLookupMode::Bound || *target_relation_bound {
387            self.resolve_rule_relation(target)?
388        } else {
389            self.resolve_visible_rule_action_relation(target)?
390        };
391        *target_relation_bound = true;
392        *target = relation.qualified_name();
393        Ok(())
394    }
395
396    pub fn rule_action_target_columns(
397        &self,
398        action: &Statement,
399    ) -> Result<std::collections::BTreeSet<String>, SQLError> {
400        crate::semantics::rules::action_binding::rule_action_target_columns(self.sources, action)
401    }
402
403    fn rule_action_target_row_type(
404        &self,
405        action: &Statement,
406    ) -> Result<Vec<(String, ColumnType)>, SQLError> {
407        crate::semantics::rules::action_binding::rule_action_target_row_type(self.sources, action)
408    }
409
410    fn validate_rule_action_definition(
411        &self,
412        action: &mut Statement,
413        event_columns: &[(String, ColumnType)],
414        event: RuleEvent,
415        lookup_mode: RelationLookupMode,
416    ) -> Result<RuleDependencies, SQLError> {
417        self.canonicalize_rule_action_target(action, lookup_mode)?;
418        let mut dependencies =
419            crate::binding::stored_relations::bind_rule_action_relation_dependencies(
420                self.relations,
421                action,
422                lookup_mode,
423            )?;
424        let action_row_type = self.rule_action_target_row_type(action)?;
425        let action_columns: std::collections::BTreeSet<String> = action_row_type
426            .iter()
427            .map(|(column, _)| column.clone())
428            .collect();
429        if lookup_mode == RelationLookupMode::Dynamic {
430            *action = crate::semantics::rules::action_binding::expand_rule_action_row_stars(
431                self.sources,
432                action,
433                &action_columns,
434                event_columns,
435                event,
436            )?;
437            *action = crate::semantics::rules::action_binding::expand_rule_action_returning_stars(
438                action,
439                &action_row_type,
440            );
441        }
442        dependencies.columns.extend(
443            crate::binding::stored_columns::bind_rule_action_column_dependencies(
444                self.columns,
445                action,
446            )?,
447        );
448        validate_rule_action_reference_scopes(self.sources, action)?;
449        let bound = crate::semantics::rules::action_binding::bind_rule_action(
450            self.sources,
451            action,
452            &action_columns,
453            &mut RuleRowTypeResolver {
454                columns: event_columns,
455                event,
456            },
457        )?;
458        let schema = crate::semantics::returning::dml_statement_returning_schema(
459            self.returning,
460            bound.clone(),
461        )?;
462        let mut stored_plan = crate::plan::UnifiedPlan::lower_with(bound, &|name: &str| {
463            self.routines.has_registered_aggregate_function(name)
464        });
465        crate::catalog::regrole_dependencies::reject_stored_plan_regrole_constants_with(
466            self.regroles,
467            &mut stored_plan,
468        )?;
469        let bound_routines = self.stored_routines.bind_statement(&stored_plan)?;
470        if let Some(routine_plan) = &bound_routines.query {
471            crate::catalog::events::dependencies::collect_query_routine_dependencies(
472                routine_plan,
473                &mut dependencies,
474            );
475        }
476        crate::catalog::stored_ast::bind_stored_statement_sites(action, &bound_routines.sites)?;
477        if let Some(schema) = schema {
478            validate_rule_returning_shape(&schema, event_columns)?;
479        }
480        Ok(dependencies)
481    }
482
483    fn bind_rule_condition_object_dependencies(
484        &self,
485        condition: &mut Expr,
486        condition_plan: Option<&crate::plan::ExpressionPlan>,
487        columns: &[(String, ColumnType)],
488        event: RuleEvent,
489        dependencies: &mut RuleDependencies,
490    ) -> Result<(), SQLError> {
491        if let Some(plan) = condition_plan {
492            if crate::type_resolution::composite_rows::expression_requires_binding(
493                condition,
494                self.routines,
495            )? {
496                let lowered =
497                    crate::plan::ExpressionPlan::lower_with(condition.clone(), &|name: &str| {
498                        self.routines.has_registered_aggregate_function(name)
499                    });
500                let sites = crate::binding::syntax_sites::expression_syntax_sites(&lowered, plan)?;
501                crate::catalog::stored_ast::bind_stored_expression_sites(condition, &sites)?;
502            }
503            crate::catalog::events::dependencies::collect_expression_routine_dependencies(
504                plan,
505                dependencies,
506            );
507            for subquery in &plan.subqueries {
508                crate::catalog::events::dependencies::collect_query_relation_dependencies(
509                    subquery,
510                    dependencies,
511                    &std::collections::BTreeSet::new(),
512                )?;
513            }
514            let routine_references =
515                crate::binding::stored_routines::collect_expression_routine_references(plan)?;
516            crate::catalog::stored_ast::bind_stored_expression_routines(
517                condition,
518                &routine_references,
519            )?;
520            return Ok(());
521        }
522
523        let bound = bind_expr(condition, &mut RuleRowTypeResolver { columns, event })?;
524        let lowered = crate::plan::ExpressionPlan::lower_with(bound, &|name: &str| {
525            self.routines.has_registered_aggregate_function(name)
526        });
527        let mut dependency_plan = lowered.clone();
528        self.stored_routines.bind_expression(
529            &mut dependency_plan,
530            &[],
531            &crate::RowSchema::default(),
532        )?;
533        crate::catalog::events::dependencies::collect_expression_routine_dependencies(
534            &dependency_plan,
535            dependencies,
536        );
537        let sites =
538            crate::binding::syntax_sites::expression_syntax_sites(&lowered, &dependency_plan)?;
539        crate::catalog::stored_ast::bind_stored_expression_sites(condition, &sites).map(|_| ())
540    }
541
542    pub fn validate_rule_definition(
543        &self,
544        definition: &mut CreateRule,
545        lookup_mode: RelationLookupMode,
546        stored_condition_plan: Option<&crate::plan::ExpressionPlan>,
547        stored_condition_binding: Option<&RuleConditionBinding>,
548    ) -> Result<
549        (
550            RelationIdentity,
551            Option<crate::plan::ExpressionPlan>,
552            Option<RuleConditionBinding>,
553            RuleDependencies,
554        ),
555        SQLError,
556    > {
557        let (relation, _) =
558            self.resolve_rule_event_relation_kind(&definition.table, lookup_mode)?;
559        definition.table = relation.qualified_name();
560        if lookup_mode == RelationLookupMode::Dynamic {
561            self.ensure_event_relation_owner(&relation, None)?;
562        }
563        let stored_view_kind = self.catalog.view_kind(&relation);
564        if stored_view_kind == Some(StoredViewKind::Materialized) {
565            return Err(SQLError::Routine {
566                sqlstate: "0A000".into(),
567                message: "rules on materialized views are not supported".into(),
568            });
569        }
570        let is_view = stored_view_kind == Some(StoredViewKind::View);
571        Self::validate_select_rule_contract(definition, is_view)?;
572        let columns = self.rule_relation_columns(&definition.table)?;
573        let mut dependencies = RuleDependencies::default();
574        if let Some(condition) = &mut definition.condition {
575            let condition_dependencies =
576                crate::binding::stored_relations::bind_rule_condition_relation_dependencies(
577                    self.relations,
578                    condition,
579                    lookup_mode,
580                )?;
581            dependencies
582                .relations
583                .extend(condition_dependencies.relations);
584            dependencies.columns.extend(
585                crate::binding::stored_columns::bind_rule_condition_column_dependencies(
586                    self.columns,
587                    condition,
588                )?,
589            );
590        }
591        let condition = definition
592            .condition
593            .as_mut()
594            .map(|condition| {
595                self.validate_rule_condition(
596                    condition,
597                    &columns,
598                    definition.event,
599                    stored_condition_plan,
600                    stored_condition_binding,
601                )
602            })
603            .transpose()?
604            .flatten();
605        let (condition_plan, condition_binding) = condition.map_or_else(
606            || (None, None),
607            |(plan, binding)| (Some(plan), Some(binding)),
608        );
609        if let Some(condition) = &mut definition.condition {
610            self.bind_rule_condition_object_dependencies(
611                condition,
612                condition_plan.as_ref(),
613                &columns,
614                definition.event,
615                &mut dependencies,
616            )?;
617            dependencies.columns.extend(
618                crate::semantics::rules::action_binding::rule_expr_row_columns(condition)
619                    .into_iter()
620                    .map(|column| crate::catalog::events::RuleColumnDependency {
621                        relation: relation.clone(),
622                        column,
623                    }),
624            );
625        }
626        validate_rule_action_contract(definition)?;
627        for action in &mut definition.actions {
628            let action_dependencies = self.validate_rule_action_definition(
629                action,
630                &columns,
631                definition.event,
632                lookup_mode,
633            )?;
634            dependencies.relations.extend(action_dependencies.relations);
635            dependencies.columns.extend(action_dependencies.columns);
636            dependencies.routines.extend(action_dependencies.routines);
637            let action_columns = self.rule_action_target_columns(action)?;
638            dependencies.columns.extend(
639                crate::semantics::rules::action_binding::rule_statement_row_columns(
640                    self.sources,
641                    action,
642                    &action_columns,
643                )?
644                .into_iter()
645                .map(|column| crate::catalog::events::RuleColumnDependency {
646                    relation: relation.clone(),
647                    column,
648                }),
649            );
650        }
651        Ok((relation, condition_plan, condition_binding, dependencies))
652    }
653}