Skip to main content

uqa_sql/prepared/
composites.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Creation-time composite descriptors for retained input constants and immutable executable caches.
8
9use crate::{
10    expr::composites::retained::{capture_descriptor, capture_type, project_value, Descriptors},
11    plan::UnifiedPlan,
12    type_resolution::FunctionTypeResolver,
13    ColumnType, SQLError, ScalarExpr,
14};
15use std::sync::Arc;
16use uqa_core::Value;
17
18/// Logical and optimized inputs retain their own descriptors and values across rollback. Reading either plan projects a copy without rerunning already-folded expressions.
19#[derive(Clone, Debug, Default)]
20pub struct CompositeInputs {
21    original: Arc<Descriptors>,
22    planned: Option<Arc<Descriptors>>,
23    planned_scope: Option<Arc<Descriptors>>,
24}
25
26impl CompositeInputs {
27    pub(super) fn has_inputs(&self) -> bool {
28        !self.original.is_empty()
29    }
30
31    pub(super) fn tracks_oid(&self, oid: u32) -> bool {
32        self.original.contains_key(&oid)
33    }
34
35    pub(super) fn tracks_type(&self, ty: &ColumnType) -> bool {
36        match ty {
37            ColumnType::Array(element) => self.tracks_type(element),
38            ColumnType::Composite(reference) => self.tracks_oid(reference.oid),
39            _ => false,
40        }
41    }
42
43    pub(super) fn tracks_name(&self, name: &str) -> bool {
44        crate::ast::UserTypeIdentity::parse(name).is_some_and(|identity| {
45            identity.kind == crate::ast::UserTypeKind::Composite && self.tracks_oid(identity.oid)
46        })
47    }
48
49    pub fn capture(plan: &UnifiedPlan, types: &dyn FunctionTypeResolver) -> Result<Self, SQLError> {
50        let mut original = Descriptors::new();
51        let mut failure = None;
52        plan.visit_scalar_expressions(&mut |expression| {
53            expression.visit(&mut |node| {
54                if failure.is_some() {
55                    return;
56                }
57                let result = match node {
58                    ScalarExpr::TypedLiteral {
59                        ty,
60                        bound_type,
61                        parameter_index: None,
62                        ..
63                    }
64                    | ScalarExpr::CompositeRow {
65                        binding: crate::ast::CompositeRowBinding { ty, .. },
66                        bound_type,
67                        ..
68                    } => match bound_type {
69                        Some(ty) => capture_type(ty, types.composite_types(), &mut original),
70                        None => types.resolve_type_name(ty).and_then(|ty| {
71                            ty.map_or(Ok(()), |ty| {
72                                capture_type(&ty, types.composite_types(), &mut original)
73                            })
74                        }),
75                    },
76                    ScalarExpr::Func {
77                        binding: Some(binding),
78                        ..
79                    } => binding.composite_field.as_ref().map_or(Ok(()), |field| {
80                        capture_descriptor(field.type_oid, types.composite_types(), &mut original)?;
81                        capture_type(&field.result_type, types.composite_types(), &mut original)
82                    }),
83                    _ => Ok(()),
84                };
85                if let Err(error) = result {
86                    failure = Some(error);
87                }
88            });
89        });
90        failure.map_or(
91            Ok(Self {
92                original: Arc::new(original),
93                planned: None,
94                planned_scope: None,
95            }),
96            Err,
97        )
98    }
99
100    /// Project original inputs before result analysis and optimization without changing the retained source.
101    pub fn project_logical(
102        &self,
103        plan: &UnifiedPlan,
104        types: &dyn FunctionTypeResolver,
105    ) -> Result<Option<UnifiedPlan>, SQLError> {
106        project_plan(plan, types, &self.original, true, None)
107    }
108
109    /// Update attribute names and field bindings for optimization while retaining the original scalar datums in the reusable executable. Interpreting a raw byte as boolean is not reversible, so an interpreted value cannot replace its source in a cache.
110    pub fn project_for_generic(
111        &self,
112        plan: &UnifiedPlan,
113        types: &dyn FunctionTypeResolver,
114    ) -> Result<Option<UnifiedPlan>, SQLError> {
115        project_plan(plan, types, &self.original, false, None)
116    }
117
118    /// Record the names and attribute numbers of the actual generic plan with the types of its retained scalar datums. Folded scalar results have no remaining composite dependency.
119    pub fn with_generic(
120        &self,
121        plan: &UnifiedPlan,
122        types: &dyn FunctionTypeResolver,
123    ) -> Result<Self, SQLError> {
124        let mut planned = Self::capture(plan, types)?.original.as_ref().clone();
125        for (oid, descriptor) in &mut planned {
126            let Some(original) = self.original.get(oid) else {
127                continue;
128            };
129            for attribute in &mut Arc::make_mut(descriptor).attributes {
130                if let Some(source) = original
131                    .attributes
132                    .iter()
133                    .find(|source| source.number == attribute.number)
134                {
135                    attribute.ty.clone_from(&source.ty);
136                }
137            }
138        }
139        Ok(Self {
140            original: self.original.clone(),
141            planned: Some(Arc::new(planned)),
142            planned_scope: Some(Arc::new(current_descriptors(&self.original, types)?)),
143        })
144    }
145
146    /// Attribute numbers cannot be reused. A number present at logical analysis, absent at optimization, and live again therefore comes from catalog undo; discard constants folded under the undone descriptor.
147    pub fn generic_requires_rebuild(
148        &self,
149        types: &dyn FunctionTypeResolver,
150    ) -> Result<bool, SQLError> {
151        let Some(planned) = &self.planned_scope else {
152            return Ok(false);
153        };
154        let current = current_descriptors(&self.original, types)?;
155        for (oid, original) in self.original.iter() {
156            let Some(at_plan) = planned.get(oid) else {
157                continue;
158            };
159            let Some(current) = current.get(oid) else {
160                continue;
161            };
162            if current.attributes.iter().any(|attribute| {
163                original
164                    .attributes
165                    .iter()
166                    .any(|old| old.number == attribute.number)
167                    && !at_plan
168                        .attributes
169                        .iter()
170                        .any(|old| old.number == attribute.number)
171            }) {
172                return Ok(true);
173            }
174        }
175        Ok(false)
176    }
177
178    /// Adapt a copy of the cached plan, preserving constants already folded by its original optimization and the source fields needed after rollback.
179    pub fn project_generic(
180        &self,
181        plan: &UnifiedPlan,
182        types: &dyn FunctionTypeResolver,
183    ) -> Result<Option<UnifiedPlan>, SQLError> {
184        self.planned.as_ref().map_or(Ok(None), |original| {
185            project_plan(plan, types, original, true, self.planned_scope.as_deref())
186        })
187    }
188}
189
190fn project_plan(
191    plan: &UnifiedPlan,
192    types: &dyn FunctionTypeResolver,
193    original: &Descriptors,
194    interpret_datums: bool,
195    binding_scope: Option<&Descriptors>,
196) -> Result<Option<UnifiedPlan>, SQLError> {
197    if original.is_empty() {
198        return Ok(None);
199    }
200    let current = current_descriptors(original, types)?;
201    let values_changed = *original != current;
202    let bindings_changed = binding_scope.is_some_and(|scope| {
203        current
204            .iter()
205            .any(|(oid, descriptor)| scope.get(oid) != Some(descriptor))
206    });
207    if !values_changed && !bindings_changed {
208        return Ok(None);
209    }
210    let mut projected = plan.clone();
211    let mut failure = None;
212    projected.rewrite_scalar_expressions(&mut |node| {
213        if failure.is_some() {
214            return;
215        }
216        if let ScalarExpr::TypedLiteral {
217            value,
218            ty,
219            parameter_index: None,
220            bound_type,
221            composite_source,
222        } = node
223        {
224            if !values_changed {
225                return;
226            }
227            let result = bound_type
228                .clone()
229                .map_or_else(|| types.resolve_type_name(ty), |ty| Ok(Some(ty)))
230                .and_then(|ty| {
231                    ty.map_or_else(
232                        || Ok(value.clone()),
233                        |ty| {
234                            project_literal(
235                                value,
236                                &ty,
237                                composite_source,
238                                original,
239                                types,
240                                interpret_datums,
241                            )
242                        },
243                    )
244                });
245            match result {
246                Ok(projected) => *value = projected,
247                Err(error) => failure = Some(error),
248            }
249        }
250        if let ScalarExpr::Func {
251            binding: Some(binding),
252            args,
253            ..
254        } = node
255        {
256            if let Some(field) = &mut binding.composite_field {
257                if let Some(descriptor) = current.get(&field.type_oid) {
258                    let attribute = descriptor
259                        .attributes
260                        .iter()
261                        .find(|attribute| attribute.number == field.number);
262                    field.dropped = attribute.is_none();
263                    field.changed_type = attribute
264                        .filter(|attribute| {
265                            crate::catalog::type_metadata::pg_type_oid(&attribute.ty)
266                                != crate::catalog::type_metadata::pg_type_oid(&field.result_type)
267                        })
268                        .map(|attribute| attribute.ty.clone());
269                    if let (Some(attribute), Some(ScalarExpr::Literal(Value::Str(name)))) =
270                        (attribute, args.get_mut(1))
271                    {
272                        name.clone_from(&attribute.name);
273                    }
274                }
275            }
276        }
277    });
278    failure.map_or(Ok(Some(projected)), Err)
279}
280
281fn project_literal(
282    value: &Value,
283    ty: &ColumnType,
284    source: &mut Option<Box<crate::expr::composites::CompositeConstantSource>>,
285    original: &Descriptors,
286    types: &dyn FunctionTypeResolver,
287    interpret_datums: bool,
288) -> Result<Value, SQLError> {
289    if !crate::expr::composites::literal::contains_records(value) {
290        return Ok(value.clone());
291    }
292    if source.is_none() {
293        *source = Some(Box::new(crate::expr::composites::CompositeConstantSource {
294            value: crate::expr::enums::retain_enum_oids(types.enum_labels(), value)?,
295            descriptors: original
296                .values()
297                .map(|value| value.as_ref().clone())
298                .collect(),
299        }));
300    }
301    let source = source.as_mut().expect("captured composite source");
302    source.retain_enum_oids(types.enum_labels())?;
303    let source_descriptors = source
304        .descriptors
305        .iter()
306        .map(|descriptor| (descriptor.type_oid, Arc::new(descriptor.clone())))
307        .collect::<Descriptors>();
308    let live = current_descriptors(&source_descriptors, types)?;
309    project_value(
310        &source.value,
311        ty,
312        &source_descriptors,
313        &live,
314        interpret_datums,
315    )
316}
317
318fn current_descriptors(
319    original: &Descriptors,
320    types: &dyn FunctionTypeResolver,
321) -> Result<Descriptors, SQLError> {
322    if original.is_empty() {
323        return Ok(Descriptors::new());
324    }
325    let catalog = types
326        .composite_types()
327        .ok_or_else(|| SQLError::Internal("prepared composite catalog unavailable".into()))?;
328    let mut current = Descriptors::new();
329    for oid in original.keys() {
330        // A removed nested type need not be read after its containing field is removed. Still-executable references report their normal missing-type error at evaluation.
331        if let Some(descriptor) = catalog.composite_type(*oid)? {
332            current.insert(*oid, descriptor);
333        }
334    }
335    Ok(current)
336}
337
338#[cfg(test)]
339fn project(
340    value: &Value,
341    ty: &ColumnType,
342    original: &Descriptors,
343    current: &Descriptors,
344) -> Result<Value, SQLError> {
345    project_value(value, ty, original, current, true)
346}
347
348#[cfg(test)]
349mod tests;