Skip to main content

uqa_sql/expr/composites/
constructor.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Evaluate retained row positions against the current composite descriptor.
8
9use crate::{
10    ast::{ColumnType, CompositeRowBinding},
11    expr::EngineHook,
12    SQLError,
13};
14use uqa_core::{
15    memory::{Produced, ProductionControl, ProductionVec},
16    Value,
17};
18
19pub fn evaluate_with_control(
20    binding: &CompositeRowBinding,
21    arguments: usize,
22    engine: Option<&dyn EngineHook>,
23    control: &ProductionControl<'_>,
24    mut evaluate: impl FnMut(usize) -> Result<Produced<Value>, SQLError>,
25) -> Result<Produced<Value>, SQLError> {
26    control.check()?;
27    if binding.attributes.len() != arguments
28        || binding.attributes.iter().any(|number| *number <= 0)
29        || binding.attributes.windows(2).any(|pair| pair[0] >= pair[1])
30    {
31        return Err(SQLError::Internal(
32            "invalid stored composite constructor positions".into(),
33        ));
34    }
35    let engine =
36        engine.ok_or_else(|| SQLError::Internal("composite constructor has no catalog".into()))?;
37    let resolved = engine
38        .resolve_type_name(&binding.ty)
39        .map_err(SQLError::Internal)?
40        .map(|ty| ty.retain_external_with_control(control))
41        .transpose()?;
42    control.check()?;
43    let Some(ColumnType::Composite(reference)) = resolved.as_deref() else {
44        return Err(SQLError::Internal(
45            "composite constructor type disappeared".into(),
46        ));
47    };
48    let descriptor = super::descriptor(engine.composite_types(), reference.oid)?;
49    let mut fields = ProductionVec::new(*control);
50    fields.reserve(descriptor.attributes.len())?;
51    for attribute in &descriptor.attributes {
52        let value = match binding.attributes.binary_search(&attribute.number) {
53            Ok(index) => evaluate(index)?,
54            Err(_) => control.finish(Value::Null, control.empty_reservation())?,
55        };
56        let name = control.copy_text(&attribute.name)?;
57        let (value, value_memory) = value.into_parts();
58        let (name, name_memory) = name.into_parts();
59        fields.push_produced(
60            control.finish((name, value), control.combine(name_memory, value_memory))?,
61        )?;
62    }
63    let (fields, memory) = fields.finish()?.into_parts();
64    control
65        .finish(Value::Record(fields), memory)
66        .map_err(Into::into)
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::ast::CompositeTypeReference;
73    use crate::expr::composites::{
74        CompositeAttribute, CompositeTypeCatalog, CompositeTypeDescriptor,
75    };
76    use std::sync::Arc;
77    use uqa_core::{memory::MemoryBudget, CancellationToken};
78
79    struct Catalog;
80    fn reference() -> ColumnType {
81        ColumnType::Composite(CompositeTypeReference {
82            schema: "public".into(),
83            name: "pair".into(),
84            oid: 20_001,
85            array_oid: 20_002,
86            relation_oid: 20_003,
87        })
88    }
89    impl CompositeTypeCatalog for Catalog {
90        fn composite_type(&self, _: u32) -> Result<Option<Arc<CompositeTypeDescriptor>>, SQLError> {
91            Ok(Some(Arc::new(CompositeTypeDescriptor {
92                type_oid: 20_001,
93                relation_oid: 20_003,
94                attributes: [1, 3, 4]
95                    .into_iter()
96                    .map(|number| CompositeAttribute {
97                        name: format!("a{number}"),
98                        ty: ColumnType::Integer,
99                        number,
100                    })
101                    .collect(),
102            })))
103        }
104    }
105    impl EngineHook for Catalog {
106        fn nextval(&self, _: &str) -> Result<i64, SQLError> {
107            unreachable!()
108        }
109        fn currval(&self, _: &str) -> Result<i64, SQLError> {
110            unreachable!()
111        }
112        fn setval(&self, _: &str, _: i64, _: bool) -> Result<i64, SQLError> {
113            unreachable!()
114        }
115        fn resolve_type_name(&self, _: &str) -> Result<Option<ColumnType>, String> {
116            Ok(Some(reference()))
117        }
118        fn composite_types(&self) -> Option<&dyn CompositeTypeCatalog> {
119            Some(self)
120        }
121    }
122
123    #[test]
124    fn stored_positions_extend_nulls_without_evaluating_removed_fields() {
125        let budget = MemoryBudget::new(4096);
126        let cancellation = CancellationToken::new();
127        let control = ProductionControl::new(&budget, &cancellation, &cancellation);
128        let binding = CompositeRowBinding {
129            ty: "composite#20001".into(),
130            attributes: vec![1, 2, 3],
131        };
132        let mut evaluated = Vec::new();
133        let value = evaluate_with_control(&binding, 3, Some(&Catalog), &control, |index| {
134            evaluated.push(index);
135            control
136                .finish(Value::Int(index as i64), control.empty_reservation())
137                .map_err(Into::into)
138        })
139        .unwrap();
140        assert_eq!(evaluated, vec![0, 2]);
141        assert_eq!(
142            *value,
143            Value::Record(vec![
144                ("a1".into(), Value::Int(0)),
145                ("a3".into(), Value::Int(2)),
146                ("a4".into(), Value::Null)
147            ])
148        );
149        assert!(budget.used() > 0);
150        drop(value);
151        assert_eq!(budget.used(), 0);
152    }
153
154    #[test]
155    fn constructor_checks_cancellation_and_admission_before_evaluating_arguments() {
156        let budget = MemoryBudget::new(0);
157        let cancellation = CancellationToken::new();
158        let control = ProductionControl::new(&budget, &cancellation, &cancellation);
159        let binding = CompositeRowBinding {
160            ty: "composite#20001".into(),
161            attributes: vec![1],
162        };
163        let error =
164            evaluate_with_control(&binding, 1, Some(&Catalog), &control, |_| unreachable!())
165                .unwrap_err();
166        assert_eq!(error.sqlstate(), Some("53200"));
167        cancellation.cancel();
168        let error =
169            evaluate_with_control(&binding, 1, Some(&Catalog), &control, |_| unreachable!())
170                .unwrap_err();
171        assert_eq!(error.sqlstate(), Some("57014"));
172    }
173}