Skip to main content

uqa_sql/semantics/rules/
analysis.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Stored-rule lineage, column requirements, and event row types.
8use super::{action_binding::RuleSourceCatalog, binding::RuleColumnMetadata, RuleCatalog};
9use crate::semantics::returning::ReturningCatalog;
10use crate::{ast::RuleEvent, SQLError};
11use std::collections::{BTreeMap, BTreeSet};
12#[derive(Clone, Copy)]
13pub struct RuleAnalysisContext<'a> {
14    pub rules: &'a dyn RuleCatalog,
15    pub sources: &'a dyn RuleSourceCatalog,
16    pub returning: &'a dyn ReturningCatalog,
17}
18pub fn rule_condition_plan_references_row(rule: &crate::catalog::events::StoredRule) -> bool {
19    rule.bound_condition_plan().is_some_and(|(plan, binding)| {
20        super::action_binding::rule_condition_plan_references_whole_row(plan)
21            || !super::action_binding::rule_condition_plan_row_columns(plan, binding).is_empty()
22    })
23}
24
25pub fn relation_suppresses_original_query(
26    context: RuleAnalysisContext<'_>,
27    table: &str,
28    event: RuleEvent,
29) -> Result<bool, SQLError> {
30    Ok(context
31        .rules
32        .rules_for(table, event)?
33        .iter()
34        .any(|rule| rule.definition.instead && rule.definition.condition.is_none()))
35}
36
37pub fn relation_rules_reference_row(
38    context: RuleAnalysisContext<'_>,
39    table: &str,
40    event: RuleEvent,
41) -> Result<bool, SQLError> {
42    for rule in context.rules.rules_for(table, event)? {
43        let condition_references_row = if rule.bound_condition_plan().is_some() {
44            rule_condition_plan_references_row(&rule)
45        } else {
46            rule.definition
47                .condition
48                .as_ref()
49                .is_some_and(super::action_binding::rule_expr_references_row)
50        };
51        if condition_references_row {
52            return Ok(true);
53        }
54        for action in &rule.definition.actions {
55            let target_columns =
56                super::action_binding::rule_action_target_columns(context.sources, action)?;
57            if super::action_binding::rule_statement_references_row(
58                context.sources,
59                action,
60                &target_columns,
61            )? {
62                return Ok(true);
63            }
64        }
65    }
66    Ok(false)
67}
68
69pub fn relation_rules_require_event_rows(
70    context: RuleAnalysisContext<'_>,
71    table: &str,
72    event: RuleEvent,
73) -> Result<bool, SQLError> {
74    Ok(context
75        .rules
76        .rules_for(table, event)?
77        .iter()
78        .any(|rule| rule.definition.condition.is_some() || !rule.definition.actions.is_empty()))
79}
80
81pub fn surviving_view_rules_reference_row(
82    context: RuleAnalysisContext<'_>,
83    relations: &[String],
84    event: RuleEvent,
85) -> Result<bool, SQLError> {
86    let mut references_row = false;
87    for relation in relations {
88        references_row |= relation_rules_reference_row(context, relation, event)?;
89        if relation_suppresses_original_query(context, relation, event)? {
90            break;
91        }
92    }
93    Ok(references_row)
94}
95
96pub fn surviving_view_rules_require_event_rows(
97    context: RuleAnalysisContext<'_>,
98    relations: &[String],
99    event: RuleEvent,
100) -> Result<bool, SQLError> {
101    let mut requires_rows = false;
102    for relation in relations {
103        requires_rows |= relation_rules_require_event_rows(context, relation, event)?;
104        if relation_suppresses_original_query(context, relation, event)? {
105            break;
106        }
107    }
108    Ok(requires_rows)
109}
110
111pub fn relation_condition_row_columns(
112    context: RuleAnalysisContext<'_>,
113    table: &str,
114    event: RuleEvent,
115) -> Result<BTreeSet<String>, SQLError> {
116    let mut columns = BTreeSet::new();
117    for rule in context.rules.rules_for(table, event)? {
118        if let Some((plan, binding)) = rule.bound_condition_plan() {
119            columns.extend(super::action_binding::rule_condition_plan_row_columns(
120                plan, binding,
121            ));
122            continue;
123        }
124        if let Some(condition) = rule.definition.condition.as_ref() {
125            columns.extend(super::action_binding::rule_expr_row_columns(condition));
126        }
127    }
128    Ok(columns)
129}
130
131pub fn relation_rule_row_columns(
132    context: RuleAnalysisContext<'_>,
133    table: &str,
134    event: RuleEvent,
135) -> Result<Option<BTreeSet<String>>, SQLError> {
136    let mut columns = BTreeSet::new();
137    let mut references_row = false;
138    let mut references_whole_row = false;
139    for rule in context.rules.rules_for(table, event)? {
140        if let Some((plan, binding)) = rule.bound_condition_plan() {
141            let plan_references_row = rule_condition_plan_references_row(&rule);
142            references_row |= plan_references_row;
143            references_whole_row |=
144                super::action_binding::rule_condition_plan_references_whole_row(plan);
145            columns.extend(super::action_binding::rule_condition_plan_row_columns(
146                plan, binding,
147            ));
148        } else if let Some(condition) = rule.definition.condition.as_ref() {
149            references_row |= super::action_binding::rule_expr_references_row(condition);
150            references_whole_row |=
151                super::action_binding::rule_expr_references_whole_row(condition);
152            columns.extend(super::action_binding::rule_expr_row_columns(condition));
153        }
154        for action in &rule.definition.actions {
155            let action_columns =
156                super::action_binding::rule_action_target_columns(context.sources, action)?;
157            references_row |= super::action_binding::rule_statement_references_row(
158                context.sources,
159                action,
160                &action_columns,
161            )?;
162            references_whole_row |= super::action_binding::rule_statement_references_whole_row(
163                context.sources,
164                action,
165                &action_columns,
166            )?;
167            columns.extend(super::action_binding::rule_statement_row_columns(
168                context.sources,
169                action,
170                &action_columns,
171            )?);
172        }
173    }
174    if references_whole_row || references_row && columns.is_empty() {
175        Ok(None)
176    } else {
177        Ok(Some(columns))
178    }
179}
180
181pub fn rule_columns(
182    context: RuleAnalysisContext<'_>,
183    table: &str,
184) -> Result<BTreeMap<String, RuleColumnMetadata>, SQLError> {
185    let columns = context
186        .returning
187        .try_describe_table_row_type(table)
188        .map_err(|error| SQLError::Internal(format!("read rule row type: {error}")))?;
189    if let Some(columns) = columns {
190        return Ok(columns
191            .into_iter()
192            .enumerate()
193            .map(|(position, column)| {
194                let uses_document_id = column.primary_key && column.ty.is_integer();
195                (
196                    column.name,
197                    RuleColumnMetadata {
198                        ty: column.ty,
199                        collation_oid: column.collation_oid,
200                        uses_document_id,
201                        position,
202                    },
203                )
204            })
205            .collect());
206    }
207    Ok(context
208        .sources
209        .rule_relation_definitions(table)?
210        .into_iter()
211        .enumerate()
212        .map(|(position, column)| {
213            (
214                column.name,
215                RuleColumnMetadata {
216                    ty: column.ty,
217                    collation_oid: column.collation_oid,
218                    uses_document_id: false,
219                    position,
220                },
221            )
222        })
223        .collect())
224}
225
226pub fn rule_returning_columns(
227    context: RuleAnalysisContext<'_>,
228    table: &str,
229) -> Result<Vec<crate::ast::ColumnDef>, SQLError> {
230    let columns = context
231        .returning
232        .try_describe_table_row_type(table)
233        .map_err(|error| SQLError::Internal(format!("read rule RETURNING row type: {error}")))?;
234    if let Some(columns) = columns {
235        return Ok(columns);
236    }
237    context.sources.rule_relation_definitions(table)
238}