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    evaluate: impl FnMut(usize) -> Result<Produced<Value>, SQLError>,
25) -> Result<Produced<Value>, SQLError> {
26    control.check()?;
27    let engine =
28        engine.ok_or_else(|| SQLError::Internal("composite constructor has no catalog".into()))?;
29    let resolved = engine
30        .resolve_type_name(&binding.ty)
31        .map_err(SQLError::Internal)?
32        .map(|ty| ty.retain_external_with_control(control))
33        .transpose()?;
34    control.check()?;
35    let Some(ColumnType::Composite(reference)) = resolved.as_deref() else {
36        return Err(SQLError::Internal(
37            "composite constructor type disappeared".into(),
38        ));
39    };
40    let descriptor = super::descriptor(engine.composite_types(), reference.oid)?;
41    construct_with_control(binding, arguments, &descriptor, control, evaluate)
42}
43
44/// Form a row using a resolved descriptor, after checking all surviving argument types and before evaluating any argument.
45pub fn construct_with_control(
46    binding: &CompositeRowBinding,
47    arguments: usize,
48    descriptor: &super::CompositeTypeDescriptor,
49    control: &ProductionControl<'_>,
50    mut evaluate: impl FnMut(usize) -> Result<Produced<Value>, SQLError>,
51) -> Result<Produced<Value>, SQLError> {
52    control.check()?;
53    if binding.attributes.len() != arguments
54        || binding.attributes.iter().any(|number| *number <= 0)
55        || binding.attributes.windows(2).any(|pair| pair[0] >= pair[1])
56    {
57        return Err(SQLError::Internal(
58            "invalid stored composite constructor positions".into(),
59        ));
60    }
61    if let Some(types) = &binding.argument_types {
62        if types.len() != arguments {
63            return Err(SQLError::Internal(
64                "invalid stored composite constructor types".into(),
65            ));
66        }
67        for attribute in &descriptor.attributes {
68            control.check()?;
69            if let Ok(index) = binding.attributes.binary_search(&attribute.number) {
70                let source = &types[index];
71                if crate::catalog::type_metadata::pg_type_oid(source)
72                    != crate::catalog::type_metadata::pg_type_oid(&attribute.ty)
73                {
74                    return Err(SQLError::Routine {
75                        sqlstate: "42804".into(),
76                        message: format!(
77                            "ROW() column has type {} instead of type {}",
78                            source.display_name(),
79                            attribute.ty.display_name()
80                        ),
81                    });
82                }
83            }
84        }
85    }
86    let mut fields = ProductionVec::new(*control);
87    fields.reserve(descriptor.attributes.len())?;
88    for attribute in &descriptor.attributes {
89        let value = match binding.attributes.binary_search(&attribute.number) {
90            Ok(index) => evaluate(index)?,
91            Err(_) => control.finish(Value::Null, control.empty_reservation())?,
92        };
93        let name = control.copy_text(&attribute.name)?;
94        let (value, value_memory) = value.into_parts();
95        let (name, name_memory) = name.into_parts();
96        fields.push_produced(
97            control.finish((name, value), control.combine(name_memory, value_memory))?,
98        )?;
99    }
100    let (fields, memory) =
101        uqa_core::RecordValue::with_control(fields.finish()?, Some(descriptor.type_oid), control)?
102            .into_parts();
103    control
104        .finish(Value::Record(fields), memory)
105        .map_err(Into::into)
106}
107
108/// Bind predecessor constructor metadata while its original descriptor is still current. Attribute removal cannot make a discarded argument run again.
109pub fn retain_argument_types(
110    binding: &mut CompositeRowBinding,
111    types: &dyn crate::type_resolution::FunctionTypeResolver,
112    catalog: Option<&dyn super::CompositeTypeCatalog>,
113) -> Result<bool, SQLError> {
114    if binding.argument_types.is_some() {
115        return Ok(false);
116    }
117    let Some(ColumnType::Composite(reference)) = types.resolve_type_name(&binding.ty)? else {
118        return Err(SQLError::Internal(
119            "composite constructor type disappeared".into(),
120        ));
121    };
122    let descriptor = super::descriptor(catalog, reference.oid)?;
123    binding.argument_types = Some(
124        binding
125            .attributes
126            .iter()
127            .map(|number| {
128                descriptor
129                    .attributes
130                    .iter()
131                    .find(|attribute| attribute.number == *number)
132                    .map_or(ColumnType::Integer, |attribute| attribute.ty.clone())
133            })
134            .collect(),
135    );
136    Ok(true)
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::ast::CompositeTypeReference;
143    use crate::expr::composites::{
144        CompositeAttribute, CompositeTypeCatalog, CompositeTypeDescriptor,
145    };
146    use std::sync::Arc;
147    use uqa_core::{memory::MemoryBudget, CancellationToken};
148
149    struct Catalog;
150    fn reference() -> ColumnType {
151        ColumnType::Composite(CompositeTypeReference {
152            schema: "public".into(),
153            name: "pair".into(),
154            oid: 20_001,
155            array_oid: 20_002,
156            relation_oid: 20_003,
157        })
158    }
159    impl CompositeTypeCatalog for Catalog {
160        fn composite_type(&self, _: u32) -> Result<Option<Arc<CompositeTypeDescriptor>>, SQLError> {
161            Ok(Some(Arc::new(CompositeTypeDescriptor {
162                dropped: Vec::new(),
163                type_oid: 20_001,
164                relation_oid: 20_003,
165                attributes: [1, 3, 4]
166                    .into_iter()
167                    .map(|number| CompositeAttribute {
168                        collation_oid: None,
169                        name: format!("a{number}"),
170                        ty: ColumnType::Integer,
171                        number,
172                    })
173                    .collect(),
174            })))
175        }
176    }
177    impl EngineHook for Catalog {
178        fn nextval(&self, _: &str) -> Result<i64, SQLError> {
179            unreachable!()
180        }
181        fn currval(&self, _: &str) -> Result<i64, SQLError> {
182            unreachable!()
183        }
184        fn setval(&self, _: &str, _: i64, _: bool) -> Result<i64, SQLError> {
185            unreachable!()
186        }
187        fn resolve_type_name(&self, _: &str) -> Result<Option<ColumnType>, String> {
188            Ok(Some(reference()))
189        }
190        fn composite_types(&self) -> Option<&dyn CompositeTypeCatalog> {
191            Some(self)
192        }
193    }
194
195    #[test]
196    fn stored_positions_extend_nulls_without_evaluating_removed_fields() {
197        let budget = MemoryBudget::new(4096);
198        let cancellation = CancellationToken::new();
199        let control = ProductionControl::new(&budget, &cancellation, &cancellation);
200        let binding = CompositeRowBinding {
201            layout: None,
202            argument_types: Some(vec![
203                ColumnType::Integer,
204                ColumnType::Text,
205                ColumnType::Integer,
206            ]),
207            ty: "composite#20001".into(),
208            attributes: vec![1, 2, 3],
209        };
210        let mut evaluated = Vec::new();
211        let value = evaluate_with_control(&binding, 3, Some(&Catalog), &control, |index| {
212            evaluated.push(index);
213            control
214                .finish(Value::Int(index as i64), control.empty_reservation())
215                .map_err(Into::into)
216        })
217        .unwrap();
218        assert_eq!(evaluated, vec![0, 2]);
219        assert_eq!(
220            *value,
221            Value::Record(
222                vec![
223                    ("a1".into(), Value::Int(0)),
224                    ("a3".into(), Value::Int(2)),
225                    ("a4".into(), Value::Null)
226                ]
227                .into()
228            )
229        );
230        assert!(budget.used() > 0);
231        drop(value);
232        assert_eq!(budget.used(), 0);
233    }
234
235    #[test]
236    fn retained_argument_types_are_checked_before_any_argument_effect() {
237        let control = ProductionControl::uncontrolled();
238        let binding = CompositeRowBinding {
239            layout: None,
240            ty: "composite#20001".into(),
241            attributes: vec![1, 2, 3],
242            argument_types: Some(vec![
243                ColumnType::Integer,
244                ColumnType::Text,
245                ColumnType::Real,
246            ]),
247        };
248        let error = evaluate_with_control(&binding, 3, Some(&Catalog), &control, |_| {
249            panic!("a type mismatch must precede every argument effect")
250        })
251        .unwrap_err();
252        assert_eq!(error.sqlstate(), Some("42804"));
253        assert_eq!(
254            error.to_string(),
255            "ROW() column has type real instead of type integer"
256        );
257    }
258
259    #[test]
260    fn constructor_checks_cancellation_and_admission_before_evaluating_arguments() {
261        let budget = MemoryBudget::new(0);
262        let cancellation = CancellationToken::new();
263        let control = ProductionControl::new(&budget, &cancellation, &cancellation);
264        let binding = CompositeRowBinding {
265            layout: None,
266            argument_types: None,
267            ty: "composite#20001".into(),
268            attributes: vec![1],
269        };
270        let error =
271            evaluate_with_control(&binding, 1, Some(&Catalog), &control, |_| unreachable!())
272                .unwrap_err();
273        assert_eq!(error.sqlstate(), Some("53200"));
274        cancellation.cancel();
275        let error =
276            evaluate_with_control(&binding, 1, Some(&Catalog), &control, |_| unreachable!())
277                .unwrap_err();
278        assert_eq!(error.sqlstate(), Some("57014"));
279    }
280    #[test]
281    fn composite_only_embedding_keeps_comparison_metadata_without_enum_support() {
282        let value = Value::Record(uqa_core::RecordValue::from_parts(
283            vec![
284                ("a1".into(), Value::Int(1)),
285                ("a3".into(), Value::Int(2)),
286                ("a4".into(), Value::Null),
287            ],
288            Some(20_001),
289        ));
290        let expression = crate::ast::Expr::Binary {
291            op: crate::ast::BinaryOp::Equal,
292            lhs: Box::new(crate::ast::Expr::Literal(value.clone())),
293            rhs: Box::new(crate::ast::Expr::Literal(value)),
294        };
295        assert!(Catalog.enum_labels().is_none());
296        let context = crate::expr::EvalContext::new(None, &[]).with_engine(&Catalog);
297        assert_eq!(
298            crate::expr::eval(&expression, &context).unwrap(),
299            Value::Bool(true)
300        );
301    }
302}