Skip to main content

uqa_sql/catalog/events/
rule_condition_binding.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Structural OLD/NEW row bindings for persisted rewrite-rule conditions.
8
9use crate::ast::{InternalColumnRef, InternalRelationId, RuleEvent};
10use crate::ir::ScalarExpr;
11use crate::plan::ExpressionPlan;
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct RuleConditionBinding {
16    old_relation: Option<InternalRelationId>,
17    new_relation: Option<InternalRelationId>,
18    columns: Vec<String>,
19}
20
21impl RuleConditionBinding {
22    pub fn for_event(columns: &[String], event: RuleEvent) -> Self {
23        let old_relation = matches!(event, RuleEvent::Update | RuleEvent::Delete)
24            .then(InternalRelationId::allocate);
25        let new_relation = matches!(event, RuleEvent::Insert | RuleEvent::Update)
26            .then(InternalRelationId::allocate);
27        Self {
28            old_relation,
29            new_relation,
30            columns: columns.to_vec(),
31        }
32    }
33
34    pub const fn old_relation(&self) -> Option<InternalRelationId> {
35        self.old_relation
36    }
37
38    pub const fn new_relation(&self) -> Option<InternalRelationId> {
39        self.new_relation
40    }
41
42    pub fn old_column(&self, name: &str) -> Option<InternalColumnRef> {
43        self.column(self.old_relation, name)
44    }
45
46    pub fn new_column(&self, name: &str) -> Option<InternalColumnRef> {
47        self.column(self.new_relation, name)
48    }
49
50    pub fn column_name(&self, column: InternalColumnRef) -> Option<&str> {
51        if Some(column.relation()) != self.old_relation
52            && Some(column.relation()) != self.new_relation
53        {
54            return None;
55        }
56        self.columns.get(column.attribute()).map(String::as_str)
57    }
58
59    /// Give a deserialized condition plan process-local row identities before it can be combined with newly planned expressions.
60    pub fn reallocate_plan_relations(&self, plan: &mut ExpressionPlan) -> Self {
61        let rebound = Self {
62            old_relation: self.old_relation.map(|_| InternalRelationId::allocate()),
63            new_relation: self.new_relation.map(|_| InternalRelationId::allocate()),
64            columns: self.columns.clone(),
65        };
66        let mut rewrite = |expression: &mut ScalarExpr| {
67            let ScalarExpr::InternalColumn(column) = expression else {
68                return;
69            };
70            let relation = if Some(column.relation()) == self.old_relation {
71                rebound.old_relation
72            } else if Some(column.relation()) == self.new_relation {
73                rebound.new_relation
74            } else {
75                None
76            };
77            if let Some(relation) = relation {
78                *column = relation.column(column.attribute());
79            }
80        };
81        crate::plan::rewrite_scalar_expression(&mut plan.scalar, &mut rewrite);
82        for subquery in &mut plan.subqueries {
83            subquery.rewrite_scalar_expressions(&mut rewrite);
84        }
85        rebound
86    }
87
88    fn column(
89        &self,
90        relation: Option<InternalRelationId>,
91        name: &str,
92    ) -> Option<InternalColumnRef> {
93        let relation = relation?;
94        self.columns
95            .iter()
96            .position(|column| column == name)
97            .map(|position| relation.column(position))
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn deserialized_plan_relations_are_reallocated_and_rewritten_together() {
107        let binding = RuleConditionBinding {
108            old_relation: Some(InternalRelationId::from_raw(u64::MAX - 1)),
109            new_relation: Some(InternalRelationId::from_raw(u64::MAX)),
110            columns: vec!["id".into()],
111        };
112        let mut plan = ExpressionPlan {
113            scalar: ScalarExpr::InternalColumn(binding.old_column("id").unwrap()),
114            subqueries: Vec::new(),
115        };
116
117        let rebound = binding.reallocate_plan_relations(&mut plan);
118        let ScalarExpr::InternalColumn(column) = plan.scalar else {
119            panic!("condition plan lost its structural OLD column")
120        };
121        assert_eq!(Some(column.relation()), rebound.old_relation());
122        assert_eq!(rebound.column_name(column), Some("id"));
123        assert_ne!(rebound.old_relation(), binding.old_relation());
124        assert_ne!(rebound.new_relation(), binding.new_relation());
125    }
126}