Skip to main content

uqa_sql/expr/composites/
constants.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Apply a descriptor change to typed catalog datums without re-reading input text or repeating domain constraints.
8
9use super::{
10    apply_attribute_change, type_contains_composite, AttributeChange, CompositeTypeCatalog,
11};
12use crate::{
13    ast::{
14        ColumnDef, ColumnType, Expr, PartitionBound, PartitionRangeDatum, PartitionSpec, Statement,
15        TableCheck,
16    },
17    plan::{QueryPlan, UnifiedPlan},
18    type_resolution::FunctionTypeResolver,
19    SQLError, ScalarExpr,
20};
21use uqa_core::Value;
22
23pub struct CompositeConstantChange<'a> {
24    pub target: u32,
25    pub change: &'a AttributeChange,
26    pub catalog: &'a dyn CompositeTypeCatalog,
27    pub types: &'a dyn FunctionTypeResolver,
28    pub rename: Option<&'a crate::binding::composite_rename::CompositeFieldRename<'a>>,
29}
30
31impl CompositeConstantChange<'_> {
32    pub fn value(&self, value: &mut Value, ty: &ColumnType) -> Result<bool, SQLError> {
33        if matches!(value, Value::Null) || !type_contains_composite(ty, self.target, self.catalog)?
34        {
35            return Ok(false);
36        }
37        *value = apply_attribute_change(
38            std::mem::replace(value, Value::Null),
39            ty,
40            self.target,
41            self.change,
42            self.catalog,
43        )?;
44        Ok(true)
45    }
46
47    fn typed_value(
48        &self,
49        value: &mut Value,
50        name: &str,
51        source: &mut Option<Box<super::CompositeConstantSource>>,
52    ) -> Result<bool, SQLError> {
53        let Some(ty) = self.types.resolve_type_name(name)? else {
54            return if crate::ast::UserTypeIdentity::parse(name).is_some() {
55                Err(SQLError::Internal(format!(
56                    "stored constant type {name} disappeared"
57                )))
58            } else {
59                Ok(false)
60            };
61        };
62        if !matches!(value, Value::Null) && type_contains_composite(&ty, self.target, self.catalog)?
63        {
64            if source.is_none() {
65                *source = Some(Box::new(super::CompositeConstantSource::capture(
66                    value,
67                    &ty,
68                    Some(self.catalog),
69                    self.types.enum_labels(),
70                )?));
71            }
72            source
73                .as_mut()
74                .expect("captured composite source")
75                .retain_enum_oids(self.types.enum_labels())?;
76            if let AttributeChange::Type { name, to, .. } = self.change {
77                *value = source
78                    .as_ref()
79                    .expect("captured composite source")
80                    .project_type_change(&ty, self.target, name, to, self.catalog)?;
81                return Ok(true);
82            }
83        }
84        self.value(value, &ty)
85    }
86
87    fn syntax_node(&self, node: &mut Expr) -> Result<bool, SQLError> {
88        match node {
89            Expr::CompositeRow { binding, .. } => {
90                super::constructor::retain_argument_types(binding, self.types, Some(self.catalog))
91            }
92            Expr::TypedLiteral {
93                value,
94                ty,
95                composite_source,
96                ..
97            } => self.typed_value(value, ty, composite_source),
98            _ => Ok(false),
99        }
100    }
101
102    fn scalar_node(&self, node: &mut ScalarExpr) -> Result<bool, SQLError> {
103        match node {
104            ScalarExpr::CompositeRow { binding, .. } => {
105                super::constructor::retain_argument_types(binding, self.types, Some(self.catalog))
106            }
107            ScalarExpr::TypedLiteral {
108                value,
109                ty,
110                composite_source,
111                ..
112            } => self.typed_value(value, ty, composite_source),
113            _ => Ok(false),
114        }
115    }
116
117    pub fn expression(&self, expression: &mut Expr) -> Result<bool, SQLError> {
118        self.expression_in_schema(expression, &crate::RowSchema::default())
119    }
120
121    pub fn expression_in_schema(
122        &self,
123        expression: &mut Expr,
124        schema: &crate::RowSchema,
125    ) -> Result<bool, SQLError> {
126        let mut changed = self
127            .rename
128            .map_or(Ok(false), |rename| rename.expression(expression, schema))?;
129        crate::catalog::stored_ast::visit_stored_expression(expression, &mut |node| {
130            changed |= self.syntax_node(node)?;
131            Ok(())
132        })?;
133        Ok(changed)
134    }
135
136    pub fn statement(&self, statement: &mut Statement) -> Result<bool, SQLError> {
137        self.statement_in_scope(statement, None, None)
138    }
139
140    pub fn statement_in_scope(
141        &self,
142        statement: &mut Statement,
143        definition: Option<&crate::ast::CreateFunction>,
144        outer: Option<&crate::RowSchema>,
145    ) -> Result<bool, SQLError> {
146        let mut changed = self.rename.map_or(Ok(false), |rename| {
147            rename.statement(statement, definition, outer)
148        })?;
149        crate::catalog::stored_ast::visit_stored_statement_expressions(statement, &mut |node| {
150            changed |= self.syntax_node(node)?;
151            Ok(())
152        })?;
153        Ok(changed)
154    }
155
156    pub fn query(&self, query: &mut QueryPlan) -> Result<bool, SQLError> {
157        let changed = self
158            .rename
159            .map_or(Ok(false), |rename| rename.query(query))?;
160        self.plan_nodes(|visit| query.rewrite_scalar_expressions(visit))
161            .map(|values| values || changed)
162    }
163
164    pub fn plan(&self, plan: &mut UnifiedPlan) -> Result<bool, SQLError> {
165        let changed = self.rename.is_some_and(|rename| rename.bound_plan(plan));
166        self.plan_nodes(|visit| plan.rewrite_scalar_expressions(visit))
167            .map(|values| values || changed)
168    }
169
170    pub fn expression_plan(
171        &self,
172        plan: &mut crate::plan::ExpressionPlan,
173    ) -> Result<bool, SQLError> {
174        let mut changed = self
175            .plan_nodes(|visit| crate::plan::rewrite_scalar_expression(&mut plan.scalar, visit))?;
176        for query in &mut plan.subqueries {
177            changed |= self.query(query)?;
178        }
179        Ok(changed)
180    }
181
182    fn plan_nodes(
183        &self,
184        visit: impl FnOnce(&mut dyn FnMut(&mut ScalarExpr)),
185    ) -> Result<bool, SQLError> {
186        let mut changed = false;
187        let mut failure = None;
188        visit(&mut |node| {
189            if failure.is_some() {
190                return;
191            }
192            match self.scalar_node(node) {
193                Ok(value) => changed |= value,
194                Err(error) => failure = Some(error),
195            }
196        });
197        failure.map_or(Ok(changed), Err)
198    }
199
200    pub fn columns(&self, columns: &mut [ColumnDef]) -> Result<bool, SQLError> {
201        let schema = crate::RowSchema::with_types(
202            columns.iter().map(|column| column.name.clone()).collect(),
203            columns
204                .iter()
205                .map(|column| Some(column.ty.clone()))
206                .collect(),
207        );
208        let mut changed = false;
209        for column in columns {
210            for expression in column.default.iter_mut().chain(column.check.iter_mut()) {
211                changed |= self.expression_in_schema(expression, &schema)?;
212            }
213            if let Some(generated) = &mut column.generated {
214                changed |= self.expression_in_schema(&mut generated.expression, &schema)?;
215            }
216        }
217        Ok(changed)
218    }
219
220    pub fn checks(
221        &self,
222        checks: &mut [TableCheck],
223        columns: &[ColumnDef],
224    ) -> Result<bool, SQLError> {
225        let schema = crate::RowSchema::with_types(
226            columns.iter().map(|column| column.name.clone()).collect(),
227            columns
228                .iter()
229                .map(|column| Some(column.ty.clone()))
230                .collect(),
231        );
232        let mut changed = false;
233        for check in checks {
234            changed |= self.expression_in_schema(&mut check.expr, &schema)?;
235            if let Some(partition) = &mut check.partition_constraint {
236                changed |= self.bound(&mut partition.bound, &partition.spec, columns)?;
237                changed |= self.spec(&mut partition.spec)?;
238            }
239        }
240        Ok(changed)
241    }
242
243    pub fn spec(&self, spec: &mut PartitionSpec) -> Result<bool, SQLError> {
244        let mut changed = false;
245        for key in &mut spec.keys {
246            changed |= self.expression(key)?;
247        }
248        Ok(changed)
249    }
250
251    pub fn bound(
252        &self,
253        bound: &mut PartitionBound,
254        spec: &PartitionSpec,
255        columns: &[ColumnDef],
256    ) -> Result<bool, SQLError> {
257        let mut changed = false;
258        for (position, key) in spec.keys.iter().enumerate() {
259            let ty = crate::semantics::partition::partition_key_type(self.types, key, columns)?;
260            match bound {
261                PartitionBound::List(values) => {
262                    for expression in values {
263                        changed |= self.bound_value(expression, &ty)?;
264                    }
265                }
266                PartitionBound::Range { lower, upper } => {
267                    for point in [lower.get_mut(position), upper.get_mut(position)]
268                        .into_iter()
269                        .flatten()
270                    {
271                        if let PartitionRangeDatum::Value(expression) = point {
272                            changed |= self.bound_value(expression, &ty)?;
273                        }
274                    }
275                }
276                PartitionBound::Default | PartitionBound::Hash { .. } => {}
277            }
278        }
279        Ok(changed)
280    }
281
282    fn bound_value(&self, expression: &mut Expr, ty: &ColumnType) -> Result<bool, SQLError> {
283        match expression {
284            Expr::Literal(value) | Expr::TypedLiteral { value, .. } => self.value(value, ty),
285            _ => Err(SQLError::Internal(
286                "partition bound datum was not evaluated".into(),
287            )),
288        }
289    }
290}
291
292#[cfg(test)]
293mod tests;