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;
21mod input;
22pub mod literal;
23pub use changes::{apply_attribute_change, type_contains_composite, AttributeChange};
24pub use input::parse_record_fields;
25
26/// One live attribute of a composite type in attribute-number order.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct CompositeAttribute {
29    pub name: String,
30    pub ty: ColumnType,
31    /// `pg_attribute.attnum`, which dropped attributes keep occupying.
32    pub number: i16,
33}
34
35/// The live attributes of one composite type in the statement's catalog generation.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct CompositeTypeDescriptor {
38    pub type_oid: u32,
39    pub relation_oid: u32,
40    pub attributes: Vec<CompositeAttribute>,
41}
42
43impl CompositeTypeDescriptor {
44    pub fn attribute(&self, name: &str) -> Option<(usize, &CompositeAttribute)> {
45        self.attributes
46            .iter()
47            .enumerate()
48            .find(|(_, attribute)| attribute.name == name)
49    }
50}
51
52/// Catalog access for composite values, supplied by the statement's execution context and by catalog-aware binding.
53pub trait CompositeTypeCatalog {
54    /// The attributes of one composite type, or `None` when the catalog has no such type.
55    fn composite_type(&self, type_oid: u32) -> Result<Option<Arc<CompositeTypeDescriptor>>>;
56}
57
58/// The attributes of a composite type the statement's catalog must contain.
59pub fn descriptor(
60    catalog: Option<&dyn CompositeTypeCatalog>,
61    type_oid: u32,
62) -> Result<Arc<CompositeTypeDescriptor>> {
63    catalog
64        .map(|catalog| catalog.composite_type(type_oid))
65        .transpose()?
66        .flatten()
67        .ok_or_else(|| {
68            SQLError::Internal(format!(
69                "composite type OID {type_oid} is not available in the statement catalog"
70            ))
71        })
72}
73
74fn cannot_cast(source: &str, target: &str, detail: Option<String>) -> SQLError {
75    SQLError::Diagnostic {
76        sqlstate: "42846".into(),
77        message: format!("cannot cast type {source} to {target}"),
78        detail,
79        hint: None,
80    }
81}
82
83fn string_category(ty: &ColumnType) -> bool {
84    match ty {
85        ColumnType::Text
86        | ColumnType::Varchar(_)
87        | ColumnType::Bpchar
88        | ColumnType::Character(_)
89        | ColumnType::Name => true,
90        ColumnType::Domain { base, .. } => string_category(base),
91        _ => false,
92    }
93}
94
95/// 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.
96fn attribute_input(
97    engine: &dyn EngineHook,
98    text: Option<String>,
99    ty: &ColumnType,
100) -> Result<Value> {
101    let value = text.map_or(Value::Null, Value::Str);
102    if matches!(value, Value::Null) && !matches!(ty, ColumnType::Domain { .. }) {
103        return Ok(Value::Null);
104    }
105    if crate::assignment::conversion::type_requires_catalog_resolution(ty) {
106        return super::cast_value_with_type_resolution(
107            &value,
108            Some("unknown"),
109            &ty.catalog_name(),
110            Some(engine),
111        );
112    }
113    crate::assignment::conversion::convert_value_to_column_type(value, ty)
114}
115
116/// `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.
117pub fn composite_from_text(engine: &dyn EngineHook, text: &str, type_oid: u32) -> Result<Value> {
118    let descriptor = descriptor(engine.composite_types(), type_oid)?;
119    let mut fields = Vec::with_capacity(descriptor.attributes.len());
120    parse_record_fields(text, descriptor.attributes.len(), |index, field| {
121        let attribute = &descriptor.attributes[index];
122        fields.push((
123            attribute.name.clone(),
124            attribute_input(engine, field, &attribute.ty)?,
125        ));
126        Ok(())
127    })?;
128    Ok(Value::Record(fields))
129}
130
131/// 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.
132fn composite_from_fields(
133    engine: &dyn EngineHook,
134    values: &[Value],
135    descriptor: &CompositeTypeDescriptor,
136    target: &str,
137) -> Result<Value> {
138    if values.len() != descriptor.attributes.len() {
139        let detail = if values.len() < descriptor.attributes.len() {
140            "Input has too few columns."
141        } else {
142            "Input has too many columns."
143        };
144        return Err(cannot_cast("record", target, Some(detail.into())));
145    }
146    values
147        .iter()
148        .zip(&descriptor.attributes)
149        .map(|(value, attribute)| {
150            let converted = super::cast_value_with_type_resolution(
151                value,
152                None,
153                &attribute.ty.catalog_name(),
154                Some(engine),
155            )?;
156            Ok((attribute.name.clone(), converted))
157        })
158        .collect::<Result<Vec<_>>>()
159        .map(Value::Record)
160}
161
162/// 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.
163pub fn cast_to_composite(
164    engine: &dyn EngineHook,
165    value: &Value,
166    source: Option<&ColumnType>,
167    target: &ColumnType,
168) -> Result<Option<Value>> {
169    let ColumnType::Composite(reference) = target else {
170        return Ok(None);
171    };
172    let target_name = || target.display_name();
173    match (value, source) {
174        (Value::Null, _) => Ok(Some(Value::Null)),
175        (Value::Str(text) | Value::FixedChar(text), source)
176            if source.is_none_or(string_category) =>
177        {
178            composite_from_text(engine, text, reference.oid).map(Some)
179        }
180        (Value::Record(_), Some(ColumnType::Composite(source))) if source.oid == reference.oid => {
181            Ok(Some(value.clone()))
182        }
183        (Value::Record(_) | Value::Row(_), Some(source @ ColumnType::Composite(_))) => {
184            Err(cannot_cast(&source.display_name(), &target_name(), None))
185        }
186        (Value::Row(values), _) => {
187            let descriptor = descriptor(engine.composite_types(), reference.oid)?;
188            composite_from_fields(engine, values, &descriptor, &target_name()).map(Some)
189        }
190        (Value::Record(fields), _) => {
191            let descriptor = descriptor(engine.composite_types(), reference.oid)?;
192            let values = fields
193                .iter()
194                .map(|(_, value)| value.clone())
195                .collect::<Vec<_>>();
196            composite_from_fields(engine, &values, &descriptor, &target_name()).map(Some)
197        }
198        (_, source) => Err(cannot_cast(
199            &source.map_or_else(|| "unknown".into(), ColumnType::display_name),
200            &target_name(),
201            None,
202        )),
203    }
204}