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                        uses_document_id,
200                        position,
201                    },
202                )
203            })
204            .collect());
205    }
206    Ok(context
207        .sources
208        .rule_relation_columns(table)?
209        .into_iter()
210        .enumerate()
211        .map(|(position, (name, ty))| {
212            (
213                name,
214                RuleColumnMetadata {
215                    ty,
216                    uses_document_id: false,
217                    position,
218                },
219            )
220        })
221        .collect())
222}
223
224pub fn rule_returning_columns(
225    context: RuleAnalysisContext<'_>,
226    table: &str,
227) -> Result<Vec<crate::ast::ColumnDef>, SQLError> {
228    let columns = context
229        .returning
230        .try_describe_table_row_type(table)
231        .map_err(|error| SQLError::Internal(format!("read rule RETURNING row type: {error}")))?;
232    if let Some(columns) = columns {
233        return Ok(columns);
234    }
235    Ok(context
236        .sources
237        .rule_relation_columns(table)?
238        .into_iter()
239        .map(|(name, ty)| crate::ast::ColumnDef {
240            name,
241            ty,
242            object_id: None,
243            missing_value: None,
244            primary_key: false,
245            not_null: false,
246            not_null_explicit: false,
247            not_null_name: None,
248            not_null_validated: true,
249            not_null_no_inherit: false,
250            not_null_is_local: true,
251            auto_increment: None,
252            unique: false,
253            default: None,
254            generated: None,
255            check: None,
256            check_name: None,
257            check_enforced: true,
258            check_validated: true,
259            check_no_inherit: false,
260            check_is_local: true,
261            check_object_id: None,
262            references: None,
263        })
264        .collect())
265}