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
18mod changes;
19pub mod constants;
20pub mod constructor;
21pub(crate) mod datum;
22pub(super) mod fields;
23mod input;
24pub mod literal;
25pub(crate) mod retained;
26pub use changes::{apply_attribute_change, type_contains_composite, AttributeChange};
27pub use input::parse_record_fields;
28pub use retained::CompositeConstantSource;
29
30/// One live attribute of a composite type in attribute-number order.
31#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32pub struct CompositeAttribute {
33    pub name: String,
34    pub ty: ColumnType,
35    /// `pg_attribute.attnum`, which dropped attributes keep occupying.
36    pub number: i16,
37}
38
39/// The live attributes of one composite type in the statement's catalog generation.
40#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
41pub struct CompositeTypeDescriptor {
42    /// Physical slots left by removed attributes, without retaining a type dependency.
43    #[serde(default, skip_serializing_if = "Vec::is_empty")]
44    pub dropped: Vec<DroppedCompositeAttribute>,
45    pub type_oid: u32,
46    pub relation_oid: u32,
47    pub attributes: Vec<CompositeAttribute>,
48}
49
50/// Width and alignment remain necessary when a retained tuple contains a removed field.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
52pub struct DroppedCompositeAttribute {
53    pub number: i16,
54    pub length: i64,
55    pub alignment: u8,
56}
57
58impl DroppedCompositeAttribute {
59    pub fn from_type(number: i16, ty: &ColumnType) -> Self {
60        Self {
61            number,
62            length: crate::catalog::type_metadata::pg_type_len(ty),
63            alignment: crate::catalog::type_metadata::pg_type_align(ty).as_bytes()[0],
64        }
65    }
66}
67
68impl CompositeTypeDescriptor {
69    pub fn attribute(&self, name: &str) -> Option<(usize, &CompositeAttribute)> {
70        self.attributes
71            .iter()
72            .enumerate()
73            .find(|(_, attribute)| attribute.name == name)
74    }
75}
76
77/// Catalog access for composite values, supplied by the statement's execution context and by catalog-aware binding.
78pub trait CompositeTypeCatalog {
79    /// The attributes of one composite type, or `None` when the catalog has no such type.
80    fn composite_type(&self, type_oid: u32) -> Result<Option<Arc<CompositeTypeDescriptor>>>;
81}
82
83/// The attributes of a composite type the statement's catalog must contain.
84pub fn descriptor(
85    catalog: Option<&dyn CompositeTypeCatalog>,
86    type_oid: u32,
87) -> Result<Arc<CompositeTypeDescriptor>> {
88    let catalog = catalog
89        .ok_or_else(|| SQLError::Internal("composite type catalog is unavailable".into()))?;
90    catalog
91        .composite_type(type_oid)?
92        .ok_or_else(|| SQLError::Routine {
93            sqlstate: "42704".into(),
94            message: format!("type with OID {type_oid} does not exist"),
95        })
96}
97
98/// `ExecEvalFieldSelect` checks the current attribute OID against the prepared result type. Call only after evaluating a non-NULL record and checking the dropped marker.
99pub fn validate_field_result(field: &crate::ast::CompositeFieldBinding) -> Result<()> {
100    let Some(current) = &field.changed_type else {
101        return Ok(());
102    };
103    validate_field_type(field, current)
104}
105
106pub(super) fn validate_field_type(
107    field: &crate::ast::CompositeFieldBinding,
108    current: &ColumnType,
109) -> Result<()> {
110    if crate::catalog::type_metadata::pg_type_oid(current)
111        == crate::catalog::type_metadata::pg_type_oid(&field.result_type)
112    {
113        return Ok(());
114    }
115    Err(SQLError::Diagnostic {
116        sqlstate: "42804".into(),
117        message: format!("attribute {} has wrong type", field.number),
118        detail: Some(format!(
119            "Table has type {}, but query expects {}.",
120            current.display_name(),
121            field.result_type.display_name(),
122        )),
123        hint: None,
124    })
125}
126
127fn cannot_cast(source: &str, target: &str, detail: Option<String>) -> SQLError {
128    SQLError::Diagnostic {
129        sqlstate: "42846".into(),
130        message: format!("cannot cast type {source} to {target}"),
131        detail,
132        hint: None,
133    }
134}
135
136fn string_category(ty: &ColumnType) -> bool {
137    match ty {
138        ColumnType::Text
139        | ColumnType::Varchar(_)
140        | ColumnType::Bpchar
141        | ColumnType::Character(_)
142        | ColumnType::Name => true,
143        ColumnType::Domain { base, .. } => string_category(base),
144        _ => false,
145    }
146}
147
148/// 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.
149fn attribute_input(
150    engine: &dyn EngineHook,
151    text: Option<String>,
152    ty: &ColumnType,
153) -> Result<Value> {
154    let value = text.map_or(Value::Null, Value::Str);
155    if matches!(value, Value::Null) && !matches!(ty, ColumnType::Domain { .. }) {
156        return Ok(Value::Null);
157    }
158    if crate::assignment::conversion::type_requires_catalog_resolution(ty) {
159        return super::cast_value_with_type_resolution(
160            &value,
161            Some("unknown"),
162            &ty.catalog_name(),
163            Some(engine),
164        );
165    }
166    crate::assignment::conversion::convert_value_to_column_type(value, ty)
167}
168
169/// `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.
170pub fn composite_from_text(engine: &dyn EngineHook, text: &str, type_oid: u32) -> Result<Value> {
171    let descriptor = descriptor(engine.composite_types(), type_oid)?;
172    let mut fields = Vec::with_capacity(descriptor.attributes.len());
173    parse_record_fields(text, descriptor.attributes.len(), |index, field| {
174        let attribute = &descriptor.attributes[index];
175        fields.push((
176            attribute.name.clone(),
177            attribute_input(engine, field, &attribute.ty)?,
178        ));
179        Ok(())
180    })?;
181    Ok(Value::Record(uqa_core::RecordValue::from_parts(
182        fields,
183        Some(type_oid),
184    )))
185}
186
187/// 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.
188fn composite_from_fields(
189    engine: &dyn EngineHook,
190    values: &[Value],
191    descriptor: &CompositeTypeDescriptor,
192    target: &str,
193) -> Result<Value> {
194    if values.len() != descriptor.attributes.len() {
195        let detail = if values.len() < descriptor.attributes.len() {
196            "Input has too few columns."
197        } else {
198            "Input has too many columns."
199        };
200        return Err(cannot_cast("record", target, Some(detail.into())));
201    }
202    values
203        .iter()
204        .zip(&descriptor.attributes)
205        .map(|(value, attribute)| {
206            let converted = super::cast_value_with_type_resolution(
207                value,
208                None,
209                &attribute.ty.catalog_name(),
210                Some(engine),
211            )?;
212            Ok((attribute.name.clone(), converted))
213        })
214        .collect::<Result<Vec<_>>>()
215        .map(|fields| {
216            Value::Record(uqa_core::RecordValue::from_parts(
217                fields,
218                Some(descriptor.type_oid),
219            ))
220        })
221}
222
223/// 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.
224pub fn cast_to_composite(
225    engine: &dyn EngineHook,
226    value: &Value,
227    source: Option<&ColumnType>,
228    target: &ColumnType,
229) -> Result<Option<Value>> {
230    let ColumnType::Composite(reference) = target else {
231        return Ok(None);
232    };
233    let target_name = || target.display_name();
234    match (value, source) {
235        (Value::Null, _) => Ok(Some(Value::Null)),
236        (Value::Str(text) | Value::FixedChar(text), source)
237            if source.is_none_or(string_category) =>
238        {
239            composite_from_text(engine, text, reference.oid).map(Some)
240        }
241        (Value::Record(fields), Some(ColumnType::Composite(source)))
242            if source.oid == reference.oid =>
243        {
244            Ok(Some(Value::Record(fields.clone().with_type_oid(Some(
245                fields.type_oid().unwrap_or(source.oid),
246            )))))
247        }
248        (Value::Datum(_), Some(ColumnType::Composite(source))) if source.oid == reference.oid => {
249            Ok(Some(value.clone()))
250        }
251        (Value::Record(_) | Value::Row(_), Some(source @ ColumnType::Composite(_))) => {
252            Err(cannot_cast(&source.display_name(), &target_name(), None))
253        }
254        (Value::Row(values), _) => {
255            let descriptor = descriptor(engine.composite_types(), reference.oid)?;
256            composite_from_fields(engine, values, &descriptor, &target_name()).map(Some)
257        }
258        (Value::Record(fields), _) => {
259            let descriptor = descriptor(engine.composite_types(), reference.oid)?;
260            let values = fields
261                .iter()
262                .map(|(_, value)| value.clone())
263                .collect::<Vec<_>>();
264            composite_from_fields(engine, &values, &descriptor, &target_name()).map(Some)
265        }
266        (_, source) => Err(cannot_cast(
267            &source.map_or_else(|| "unknown".into(), ColumnType::display_name),
268            &target_name(),
269            None,
270        )),
271    }
272}