Skip to main content

uqa_sql/semantics/
rules.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Rewrite-rule declaration and RETURNING contracts.
8
9use crate::{
10    ast::{RuleEvent, Statement},
11    catalog::events::StoredRule,
12    SQLError,
13};
14use uqa_core::RelationIdentity;
15
16/// Resolve stored rule definitions and a mutation's canonical target.
17pub trait RuleCatalog {
18    fn relation_has_rules(&self, table: &str) -> Result<bool, SQLError>;
19    fn resolve_rule_relation(&self, table: &str) -> Result<RelationIdentity, SQLError>;
20    fn rules_for(&self, table: &str, event: RuleEvent) -> Result<Vec<StoredRule>, SQLError>;
21    fn resolve_mutation_target(&self, name: &str, bound: bool) -> Result<String, SQLError>;
22}
23
24pub mod binding;
25pub fn validate_rule_returning_contract(
26    catalog: &dyn RuleCatalog,
27    table: &str,
28    event: RuleEvent,
29    requested: bool,
30) -> Result<(), SQLError> {
31    if !requested {
32        return Ok(());
33    }
34    let table = catalog.resolve_rule_relation(table)?.qualified_name();
35    let rules = catalog.rules_for(&table, event)?;
36    if rules.is_empty() || !rules.iter().any(|rule| rule.definition.instead) {
37        return Ok(());
38    }
39    let providers = rules
40        .iter()
41        .flat_map(|rule| &rule.definition.actions)
42        .filter(|action| statement_has_returning(action))
43        .count();
44    if providers > 1 {
45        return Err(SQLError::Routine {
46            sqlstate: "0A000".into(),
47            message: "cannot have RETURNING lists in multiple rules".into(),
48        });
49    }
50    if providers == 1 {
51        return Ok(());
52    }
53    let relation = RelationIdentity::from_legacy_name(&table)
54        .map_err(|error| SQLError::Internal(format!("decode rule relation `{table}`: {error}")))?;
55    let event = rule_event_name(event);
56    Err(SQLError::Diagnostic {
57        sqlstate: "0A000".into(),
58        message: format!(
59            "cannot perform {event} RETURNING on relation \"{}\"",
60            relation.name
61        ),
62        detail: None,
63        hint: Some(format!(
64            "You need an unconditional ON {event} DO INSTEAD rule with a RETURNING clause."
65        )),
66    })
67}
68
69const fn rule_event_name(event: RuleEvent) -> &'static str {
70    match event {
71        RuleEvent::Select => "SELECT",
72        RuleEvent::Insert => "INSERT",
73        RuleEvent::Update => "UPDATE",
74        RuleEvent::Delete => "DELETE",
75    }
76}
77
78pub fn statement_has_returning(statement: &Statement) -> bool {
79    match statement {
80        Statement::Insert(statement) => !statement.returning.is_empty(),
81        Statement::Update(statement) => !statement.returning.is_empty(),
82        Statement::Delete(statement) => !statement.returning.is_empty(),
83        _ => false,
84    }
85}
86
87pub fn clear_statement_returning(statement: &mut Statement) {
88    match statement {
89        Statement::Insert(statement) => statement.returning.clear(),
90        Statement::Update(statement) => statement.returning.clear(),
91        Statement::Delete(statement) => statement.returning.clear(),
92        _ => {}
93    }
94}
95
96pub mod action_binding;
97
98pub mod analysis;
99pub mod returning;
100
101pub mod insert_inputs;