Skip to main content

uqa_sql/semantics/view_rewrite/
rule_inputs.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Surviving input dependencies across automatic views and rewrite rules.
8
9use super::{
10    automatic_view_layer, has_instead_of_trigger, BTreeSet, SQLError, TriggerEvent,
11    ViewRewriteContext,
12};
13use crate::ast::RuleEvent;
14
15pub struct RuleInputRequirements {
16    pub columns: BTreeSet<String>,
17    pub requires_rows: bool,
18}
19
20pub fn rule_input_requirements(
21    services: ViewRewriteContext<'_>,
22    table: &str,
23    event: RuleEvent,
24) -> Result<Option<RuleInputRequirements>, SQLError> {
25    collect_requirements(services, table, event, &mut BTreeSet::new())
26}
27
28fn collect_requirements(
29    services: ViewRewriteContext<'_>,
30    table: &str,
31    event: RuleEvent,
32    visited: &mut BTreeSet<String>,
33) -> Result<Option<RuleInputRequirements>, SQLError> {
34    if !visited.insert(table.to_string()) {
35        return Err(SQLError::Internal(format!(
36            "cycle while resolving rewrite-rule inputs for `{table}`"
37        )));
38    }
39    let rules = services.catalog.rules_for(table, event)?;
40    let suppresses = rules
41        .iter()
42        .any(|rule| rule.definition.instead && rule.definition.condition.is_none());
43    let mut required = RuleInputRequirements {
44        columns: BTreeSet::new(),
45        requires_rows: rules
46            .iter()
47            .any(|rule| rule.definition.condition.is_some() || !rule.definition.actions.is_empty()),
48    };
49    for rule in &rules {
50        let Some(columns) = services.catalog.rule_new_row_columns(rule)? else {
51            return Ok(None);
52        };
53        required.columns.extend(columns);
54    }
55    if suppresses {
56        return Ok(Some(required));
57    }
58    let trigger = match event {
59        RuleEvent::Insert => TriggerEvent::Insert,
60        RuleEvent::Update => TriggerEvent::Update,
61        RuleEvent::Delete => TriggerEvent::Delete,
62        RuleEvent::Select => return Ok(None),
63    };
64    if has_instead_of_trigger(services, table, trigger)? {
65        return Ok(None);
66    }
67    let Some(layer) = automatic_view_layer(services, table)? else {
68        return Ok(None);
69    };
70    let Some(source) = collect_requirements(services, &layer.source_name, event, visited)? else {
71        return Ok(None);
72    };
73    required.requires_rows |= source.requires_rows;
74    for column in layer.columns {
75        if column
76            .writable_source_column
77            .as_ref()
78            .is_some_and(|name| source.columns.contains(name))
79        {
80            required.columns.insert(column.name);
81        }
82    }
83    Ok(Some(required))
84}
85
86/// `PostgreSQL` cannot substitute a `MultiAssignRef` for a rule's NEW variable.
87pub fn validate_grouped_rule_inputs(
88    services: ViewRewriteContext<'_>,
89    table: &str,
90    assignments: &[crate::plan::AssignmentPlan],
91) -> Result<(), SQLError> {
92    let mut columns = assignments
93        .iter()
94        .filter(|assignment| {
95            matches!(
96                assignment.target,
97                crate::ast::AssignmentTargets::Multiple(_)
98            )
99        })
100        .flat_map(|assignment| assignment.target.column_names())
101        .map(str::to_owned)
102        .collect::<BTreeSet<_>>();
103    let mut table = table.to_owned();
104    let mut visited = BTreeSet::new();
105    while !columns.is_empty() && visited.insert(table.clone()) {
106        let rules = services.catalog.rules_for(&table, RuleEvent::Update)?;
107        for rule in &rules {
108            if services
109                .catalog
110                .rule_new_row_columns(rule)?
111                .is_none_or(|new| !new.is_disjoint(&columns))
112            {
113                return Err(SQLError::Routine {
114                    sqlstate: "0A000".into(),
115                    message: "NEW variables in ON UPDATE rules cannot reference columns that are part of a multiple assignment in the subject UPDATE command".into(),
116                });
117            }
118        }
119        if rules
120            .iter()
121            .any(|rule| rule.definition.instead && rule.definition.condition.is_none())
122            || has_instead_of_trigger(services, &table, TriggerEvent::Update)?
123        {
124            break;
125        }
126        let Some(layer) = automatic_view_layer(services, &table)? else {
127            break;
128        };
129        columns = layer
130            .columns
131            .into_iter()
132            .filter(|column| columns.contains(&column.name))
133            .filter_map(|column| column.writable_source_column)
134            .collect();
135        table = layer.source_name;
136    }
137    Ok(())
138}