Skip to main content

uqa_sql/expr/
composites.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Composite type values through the statement's catalog: attribute metadata, `record_in` input and the coercion of rows and records to a composite type.
8//!
9//! A composite value is a [`Value::Record`] whose fields carry the type's current attribute names in attribute order. Anonymous row constructors are [`Value::Row`] until a coercion gives them a composite type.
10
11use std::sync::Arc;
12
13use uqa_core::Value;
14
15use super::{EngineHook, Result, SQLError};
16use crate::ast::ColumnType;
17
18#[cfg(test)]
19mod assignment_tests;
20mod changes;
21pub mod constants;
22pub mod constructor;
23pub(crate) mod datum;
24pub(super) mod fields;
25mod input;
26pub mod literal;
27pub(crate) mod retained;
28pub use changes::{apply_attribute_change, type_contains_composite, AttributeChange};
29pub use input::parse_record_fields;
30pub use retained::CompositeConstantSource;
31
32/// One live attribute of a composite type in attribute-number order.
33#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
34pub struct CompositeAttribute {
35    pub name: String,
36    pub ty: ColumnType,
37    /// Selected declaration identity; absent metadata uses the attribute type's default.
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub collation_oid: Option<u32>,
40    /// `pg_attribute.attnum`, which dropped attributes keep occupying.
41    pub number: i16,
42}
43
44impl CompositeAttribute {
45    #[must_use]
46    pub fn collation_oid(&self) -> u32 {
47        self.collation_oid.unwrap_or_else(|| {
48            u32::try_from(crate::catalog::type_metadata::pg_type_collation_oid(
49                &self.ty,
50            ))
51            .expect("type collation identity")
52        })
53    }
54}
55
56/// The live attributes of one composite type in the statement's catalog generation.
57#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
58pub struct CompositeTypeDescriptor {
59    /// Physical slots left by removed attributes, without retaining a type dependency.
60    #[serde(default, skip_serializing_if = "Vec::is_empty")]
61    pub dropped: Vec<DroppedCompositeAttribute>,
62    pub type_oid: u32,
63    pub relation_oid: u32,
64    pub attributes: Vec<CompositeAttribute>,
65}
66
67/// Width and alignment remain necessary when a retained tuple contains a removed field.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
69pub struct DroppedCompositeAttribute {
70    pub number: i16,
71    pub length: i64,
72    pub alignment: u8,
73}
74
75impl DroppedCompositeAttribute {
76    pub fn from_type(number: i16, ty: &ColumnType) -> Self {
77        Self {
78            number,
79            length: crate::catalog::type_metadata::pg_type_len(ty),
80            alignment: crate::catalog::type_metadata::pg_type_align(ty).as_bytes()[0],
81        }
82    }
83}
84
85impl CompositeTypeDescriptor {
86    /// Number of physical positions, including trailing dropped attributes.
87    pub fn physical_width(&self) -> i16 {
88        self.attributes
89            .iter()
90            .map(|attribute| attribute.number)
91            .chain(self.dropped.iter().map(|attribute| attribute.number))
92            .max()
93            .unwrap_or(0)
94    }
95
96    pub fn attribute(&self, name: &str) -> Option<(usize, &CompositeAttribute)> {
97        self.attributes
98            .iter()
99            .enumerate()
100            .find(|(_, attribute)| attribute.name == name)
101    }
102}
103
104/// Catalog access for composite values, supplied by the statement's execution context and by catalog-aware binding.
105pub trait CompositeTypeCatalog {
106    /// The attributes of one composite type, or `None` when the catalog has no such type.
107    fn composite_type(&self, type_oid: u32) -> Result<Option<Arc<CompositeTypeDescriptor>>>;
108}
109
110/// The attributes of a composite type the statement's catalog must contain.
111pub fn descriptor(
112    catalog: Option<&dyn CompositeTypeCatalog>,
113    type_oid: u32,
114) -> Result<Arc<CompositeTypeDescriptor>> {
115    let catalog = catalog
116        .ok_or_else(|| SQLError::Internal("composite type catalog is unavailable".into()))?;
117    catalog
118        .composite_type(type_oid)?
119        .ok_or_else(|| SQLError::Routine {
120            sqlstate: "42704".into(),
121            message: format!("type with OID {type_oid} does not exist"),
122        })
123}
124
125/// `ExecEvalFieldSelect` checks the current attribute OID against the prepared result type. Call only after evaluating a non-NULL record and checking the dropped marker.
126pub fn validate_field_result(field: &crate::ast::CompositeFieldBinding) -> Result<()> {
127    let Some(current) = &field.changed_type else {
128        return Ok(());
129    };
130    validate_field_type(field, current)
131}
132
133pub(super) fn validate_field_type(
134    field: &crate::ast::CompositeFieldBinding,
135    current: &ColumnType,
136) -> Result<()> {
137    if crate::catalog::type_metadata::pg_type_oid(current)
138        == crate::catalog::type_metadata::pg_type_oid(&field.result_type)
139    {
140        return Ok(());
141    }
142    Err(SQLError::Diagnostic {
143        sqlstate: "42804".into(),
144        message: format!("attribute {} has wrong type", field.number),
145        detail: Some(format!(
146            "Table has type {}, but query expects {}.",
147            current.display_name(),
148            field.result_type.display_name(),
149        )),
150        hint: None,
151    })
152}
153
154fn cannot_cast(source: &str, target: &str, detail: Option<String>) -> SQLError {
155    SQLError::Diagnostic {
156        sqlstate: "42846".into(),
157        message: format!("cannot cast type {source} to {target}"),
158        detail,
159        hint: None,
160    }
161}
162
163fn string_category(ty: &ColumnType) -> bool {
164    match ty {
165        ColumnType::Text
166        | ColumnType::Varchar(_)
167        | ColumnType::Bpchar
168        | ColumnType::Character(_)
169        | ColumnType::Name => true,
170        ColumnType::Domain { base, .. } => string_category(base),
171        _ => false,
172    }
173}
174
175/// Convert one attribute's text with the attribute type's input function and type modifier, which rejects a value too long for its declared length instead of truncating it as an explicit cast does. A NULL field still reaches a domain's input function, which enforces the domain's NOT NULL constraint.
176fn attribute_input(
177    engine: &dyn EngineHook,
178    text: Option<String>,
179    ty: &ColumnType,
180) -> Result<Value> {
181    let value = text.map_or(Value::Null, Value::Str);
182    if matches!(value, Value::Null) && !matches!(ty, ColumnType::Domain { .. }) {
183        return Ok(Value::Null);
184    }
185    if crate::assignment::conversion::type_requires_catalog_resolution(ty) {
186        return super::cast_value_with_type_resolution(
187            &value,
188            Some("unknown"),
189            &ty.catalog_name(),
190            Some(engine),
191        );
192    }
193    crate::assignment::conversion::convert_value_to_column_type(value, ty)
194}
195
196/// `record_in` for a composite type: the fields are split and each is converted in order, so a field's input error precedes a later structural error.
197pub fn composite_from_text(engine: &dyn EngineHook, text: &str, type_oid: u32) -> Result<Value> {
198    let descriptor = descriptor(engine.composite_types(), type_oid)?;
199    let mut fields = Vec::with_capacity(descriptor.attributes.len());
200    parse_record_fields(text, descriptor.attributes.len(), |index, field| {
201        let attribute = &descriptor.attributes[index];
202        fields.push((
203            attribute.name.clone(),
204            attribute_input(engine, field, &attribute.ty)?,
205        ));
206        Ok(())
207    })?;
208    Ok(Value::Record(uqa_core::RecordValue::from_parts(
209        fields,
210        Some(type_oid),
211    )))
212}
213
214/// Coerce the fields of an anonymous row to a composite type position by position, as `coerce_record_to_complex` does. The field count must match the live attributes.
215fn composite_from_fields(
216    engine: &dyn EngineHook,
217    values: &[Value],
218    field_types: Option<&[uqa_core::RecordFieldType]>,
219    descriptor: &CompositeTypeDescriptor,
220    target: &str,
221) -> Result<Value> {
222    if values.len() != descriptor.attributes.len() {
223        let detail = if values.len() < descriptor.attributes.len() {
224            "Input has too few columns."
225        } else {
226            "Input has too many columns."
227        };
228        return Err(cannot_cast("record", target, Some(detail.into())));
229    }
230    values
231        .iter()
232        .zip(&descriptor.attributes)
233        .enumerate()
234        .map(|(index, (value, attribute))| {
235            let source = field_types
236                .and_then(|types| types.get(index))
237                .filter(|field| field.oid != 705)
238                .map(|field| {
239                    if let Some(ty) = crate::catalog::type_metadata::builtin_scalar_type(field.oid)
240                    {
241                        return Ok(ty.clone());
242                    }
243                    if let Some(element) =
244                        crate::catalog::type_metadata::builtin_array_element(field.oid)
245                    {
246                        return Ok(ColumnType::Array(Box::new(element.clone())));
247                    }
248                    engine
249                        .resolve_type_oid(field.oid)
250                        .map_err(SQLError::Internal)?
251                        .ok_or_else(|| SQLError::Routine {
252                            sqlstate: "42704".into(),
253                            message: format!("type with OID {} does not exist", field.oid),
254                        })
255                })
256                .transpose()?
257                .map(|ty| ty.catalog_name());
258            let converted = super::cast_value_with_type_resolution(
259                value,
260                source.as_deref(),
261                &attribute.ty.catalog_name(),
262                Some(engine),
263            )?;
264            Ok((attribute.name.clone(), converted))
265        })
266        .collect::<Result<Vec<_>>>()
267        .map(|fields| {
268            Value::Record(uqa_core::RecordValue::from_parts(
269                fields,
270                Some(descriptor.type_oid),
271            ))
272        })
273}
274
275/// Cast a value to a composite type: the identity for a value of the same type, `record_in` for unknown literals and string-category sources, and attribute-wise coercion for anonymous rows and records. Returns `None` when the target is not a composite type.
276pub fn cast_to_composite(
277    engine: &dyn EngineHook,
278    value: &Value,
279    source: Option<&ColumnType>,
280    target: &ColumnType,
281) -> Result<Option<Value>> {
282    let ColumnType::Composite(reference) = target else {
283        return Ok(None);
284    };
285    let target_name = || target.display_name();
286    match (value, source) {
287        (Value::Null, _) => Ok(Some(Value::Null)),
288        (Value::Str(text) | Value::FixedChar(text), source)
289            if source.is_none_or(string_category) =>
290        {
291            composite_from_text(engine, text, reference.oid).map(Some)
292        }
293        (Value::Record(fields), Some(ColumnType::Composite(source)))
294            if source.oid == reference.oid =>
295        {
296            Ok(Some(Value::Record(fields.clone().with_type_oid(Some(
297                fields.type_oid().unwrap_or(source.oid),
298            )))))
299        }
300        (Value::Datum(_), Some(ColumnType::Composite(source))) if source.oid == reference.oid => {
301            Ok(Some(value.clone()))
302        }
303        (Value::Record(_) | Value::Row(_), Some(source @ ColumnType::Composite(_))) => {
304            Err(cannot_cast(&source.display_name(), &target_name(), None))
305        }
306        (Value::Row(values), _) => {
307            let descriptor = descriptor(engine.composite_types(), reference.oid)?;
308            composite_from_fields(
309                engine,
310                values,
311                values.field_types(),
312                &descriptor,
313                &target_name(),
314            )
315            .map(Some)
316        }
317        (Value::Record(fields), _) => {
318            let descriptor = descriptor(engine.composite_types(), reference.oid)?;
319            let values = fields
320                .iter()
321                .map(|(_, value)| value.clone())
322                .collect::<Vec<_>>();
323            composite_from_fields(engine, &values, None, &descriptor, &target_name()).map(Some)
324        }
325        (_, source) => Err(cannot_cast(
326            &source.map_or_else(|| "unknown".into(), ColumnType::display_name),
327            &target_name(),
328            None,
329        )),
330    }
331}