1use 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#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct CompositeAttribute {
29 pub name: String,
30 pub ty: ColumnType,
31 pub number: i16,
33}
34
35#[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
52pub trait CompositeTypeCatalog {
54 fn composite_type(&self, type_oid: u32) -> Result<Option<Arc<CompositeTypeDescriptor>>>;
56}
57
58pub 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
95fn 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
116pub 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
131fn 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
162pub 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}