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            ..
223        } = node
224        {
225            if !values_changed {
226                return;
227            }
228            let result = bound_type
229                .clone()
230                .map_or_else(|| types.resolve_type_name(ty), |ty| Ok(Some(ty)))
231                .and_then(|ty| {
232                    ty.map_or_else(
233                        || Ok(value.clone()),
234                        |ty| {
235                            project_literal(
236                                value,
237                                &ty,
238                                composite_source,
239                                original,
240                                types,
241                                interpret_datums,
242                            )
243                        },
244                    )
245                });
246            match result {
247                Ok(projected) => *value = projected,
248                Err(error) => failure = Some(error),
249            }
250        }
251        if let ScalarExpr::Func {
252            binding: Some(binding),
253            args,
254            ..
255        } = node
256        {
257            if let Some(field) = &mut binding.composite_field {
258                if let Some(descriptor) = current.get(&field.type_oid) {
259                    let attribute = descriptor
260                        .attributes
261                        .iter()
262                        .find(|attribute| attribute.number == field.number);
263                    field.dropped = attribute.is_none();
264                    field.changed_type = attribute
265                        .filter(|attribute| {
266                            crate::catalog::type_metadata::pg_type_oid(&attribute.ty)
267                                != crate::catalog::type_metadata::pg_type_oid(&field.result_type)
268                        })
269                        .map(|attribute| attribute.ty.clone());
270                    if let (Some(attribute), Some(ScalarExpr::Literal(Value::Str(name)))) =
271                        (attribute, args.get_mut(1))
272                    {
273                        name.clone_from(&attribute.name);
274                    }
275                }
276            }
277        }
278    });
279    failure.map_or(Ok(Some(projected)), Err)
280}
281
282fn project_literal(
283    value: &Value,
284    ty: &ColumnType,
285    source: &mut Option<Box<crate::expr::composites::CompositeConstantSource>>,
286    original: &Descriptors,
287    types: &dyn FunctionTypeResolver,
288    interpret_datums: bool,
289) -> Result<Value, SQLError> {
290    if !crate::expr::composites::literal::contains_records(value) {
291        return Ok(value.clone());
292    }
293    if source.is_none() {
294        *source = Some(Box::new(crate::expr::composites::CompositeConstantSource {
295            value: crate::expr::enums::retain_enum_oids(types.enum_labels(), value)?,
296            descriptors: original
297                .values()
298                .map(|value| value.as_ref().clone())
299                .collect(),
300        }));
301    }
302    let source = source.as_mut().expect("captured composite source");
303    source.retain_enum_oids(types.enum_labels())?;
304    let source_descriptors = source
305        .descriptors
306        .iter()
307        .map(|descriptor| (descriptor.type_oid, Arc::new(descriptor.clone())))
308        .collect::<Descriptors>();
309    let live = current_descriptors(&source_descriptors, types)?;
310    project_value(
311        &source.value,
312        ty,
313        &source_descriptors,
314        &live,
315        interpret_datums,
316    )
317}
318
319fn current_descriptors(
320    original: &Descriptors,
321    types: &dyn FunctionTypeResolver,
322) -> Result<Descriptors, SQLError> {
323    if original.is_empty() {
324        return Ok(Descriptors::new());
325    }
326    let catalog = types
327        .composite_types()
328        .ok_or_else(|| SQLError::Internal("prepared composite catalog unavailable".into()))?;
329    let mut current = Descriptors::new();
330    for oid in original.keys() {
331        // 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.
332        if let Some(descriptor) = catalog.composite_type(*oid)? {
333            current.insert(*oid, descriptor);
334        }
335    }
336    Ok(current)
337}
338
339#[cfg(test)]
340fn project(
341    value: &Value,
342    ty: &ColumnType,
343    original: &Descriptors,
344    current: &Descriptors,
345) -> Result<Value, SQLError> {
346    project_value(value, ty, original, current, true)
347}
348
349#[cfg(test)]
350mod tests;