1use 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
44pub 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
108pub 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}