Skip to main content

uqa_sql/semantics/rules/
binding.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! OLD/NEW value binding for set-oriented rewrite-rule actions.
8
9use crate::{
10    ast::Expr,
11    plpgsql::{ResolvedVariable, VariableResolver},
12    ResultRow as Document, SQLError,
13};
14use std::collections::BTreeMap;
15use uqa_core::{DocId, Value};
16
17/// Logical OLD/NEW values used by rule action binding. Physical origins and source contexts stay in the executor.
18pub trait RuleRowValues {
19    fn old_row(&self) -> Option<&Document>;
20    fn new_row(&self) -> Option<&Document>;
21    fn old_doc_id(&self) -> Option<DocId>;
22    fn new_doc_id(&self) -> Option<DocId>;
23}
24
25mod actions;
26pub use actions::{bind_insert_values_action, bind_set_oriented_action, BoundSetOrientedAction};
27
28pub struct RuleColumnMetadata {
29    pub ty: crate::ast::ColumnType,
30    pub collation_oid: Option<u32>,
31    pub uses_document_id: bool,
32    pub position: usize,
33}
34
35impl RuleColumnMetadata {
36    /// A projected OLD/NEW field is still a column of the event relation. Its source expression cannot export explicit collation strength across that boundary.
37    pub fn bind_input(&self, expression: crate::ScalarExpr) -> crate::ScalarExpr {
38        if !crate::schema::composites::type_is_collatable(&self.ty) {
39            return expression;
40        }
41        let oid = self.collation_oid.unwrap_or_else(|| {
42            u32::try_from(crate::catalog::type_metadata::pg_type_collation_oid(
43                &self.ty,
44            ))
45            .expect("type collation identity")
46        });
47        crate::ScalarExpr::Collated {
48            expr: Box::new(expression),
49            ty: self.ty.clone(),
50            collation: crate::ir::ResultCollation::new(
51                crate::catalog::collations::derivation::Derivation::implicit(i64::from(oid)),
52                oid,
53            ),
54        }
55    }
56}
57
58pub(super) struct RuntimeRuleResolver<'a> {
59    pub(super) old: Option<&'a Document>,
60    pub(super) new: Option<&'a Document>,
61    pub(super) old_doc_id: Option<DocId>,
62    pub(super) new_doc_id: Option<DocId>,
63    pub(super) columns: &'a BTreeMap<String, RuleColumnMetadata>,
64}
65
66impl RuntimeRuleResolver<'_> {
67    pub(super) fn field_expression(
68        &self,
69        record: Option<&Document>,
70        doc_id: Option<DocId>,
71        column: &str,
72    ) -> Result<Expr, SQLError> {
73        self.record_field(record, doc_id, column)
74            .map(ResolvedVariable::into_expression)
75    }
76
77    pub(super) fn record_field(
78        &self,
79        record: Option<&Document>,
80        doc_id: Option<DocId>,
81        column: &str,
82    ) -> Result<ResolvedVariable, SQLError> {
83        let (metadata, value) = self.field(record, doc_id, column)?;
84        Ok(ResolvedVariable {
85            collation_oid: metadata.collation_oid,
86            value,
87            declared_type: Some(metadata.ty.catalog_name()),
88        })
89    }
90
91    fn field(
92        &self,
93        record: Option<&Document>,
94        doc_id: Option<DocId>,
95        column: &str,
96    ) -> Result<(&RuleColumnMetadata, Value), SQLError> {
97        let metadata = self
98            .columns
99            .get(column)
100            .ok_or_else(|| SQLError::UnknownColumn(column.to_string()))?;
101        let value = if let Some(value) = record.and_then(|record| record.get(column).cloned()) {
102            value
103        } else if metadata.uses_document_id {
104            doc_id
105                .filter(|doc_id| crate::semantics::key_identity::is_key_document_id(*doc_id))
106                .map(i64::try_from)
107                .transpose()
108                .map_err(|_| {
109                    SQLError::TypeMismatch("document id exceeds PostgreSQL bigint".into())
110                })?
111                .map_or(Value::Null, Value::Int)
112        } else {
113            Value::Null
114        };
115        Ok((metadata, value))
116    }
117
118    pub(super) fn record(
119        &self,
120        record: Option<&Document>,
121        doc_id: Option<DocId>,
122    ) -> Result<ResolvedVariable, SQLError> {
123        let mut columns = self.columns.iter().collect::<Vec<_>>();
124        columns.sort_by_key(|(_, metadata)| metadata.position);
125        let fields = columns
126            .into_iter()
127            .map(|(column, _)| {
128                self.record_field(record, doc_id, column)
129                    .map(|field| (column.clone(), field.value))
130            })
131            .collect::<Result<Vec<_>, _>>()?;
132        Ok(ResolvedVariable::untyped(Value::Record(fields.into())))
133    }
134}
135
136impl VariableResolver for RuntimeRuleResolver<'_> {
137    fn resolve_name(&mut self, name: &str) -> Result<Option<ResolvedVariable>, SQLError> {
138        if name.eq_ignore_ascii_case("old") {
139            return self.record(self.old, self.old_doc_id).map(Some);
140        }
141        if name.eq_ignore_ascii_case("new") {
142            return self.record(self.new, self.new_doc_id).map(Some);
143        }
144        Ok(None)
145    }
146
147    fn resolve_qualified(
148        &mut self,
149        qualifier: &str,
150        column: &str,
151    ) -> Result<Option<ResolvedVariable>, SQLError> {
152        if qualifier.eq_ignore_ascii_case("old") {
153            return self
154                .record_field(self.old, self.old_doc_id, column)
155                .map(Some);
156        }
157        if qualifier.eq_ignore_ascii_case("new") {
158            return self
159                .record_field(self.new, self.new_doc_id, column)
160                .map(Some);
161        }
162        Ok(None)
163    }
164
165    fn resolve_param(&mut self, _index: usize) -> Result<Option<ResolvedVariable>, SQLError> {
166        Ok(None)
167    }
168
169    fn rewrite_qualified_whole_row(&mut self, qualifier: &str) -> Result<Option<Expr>, SQLError> {
170        Ok(self
171            .resolve_name(qualifier)?
172            .map(|record| Expr::Literal(record.value)))
173    }
174}