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;
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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32pub struct CompositeAttribute {
33 pub name: String,
34 pub ty: ColumnType,
35 pub number: i16,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
41pub struct CompositeTypeDescriptor {
42 #[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#[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
77pub trait CompositeTypeCatalog {
79 fn composite_type(&self, type_oid: u32) -> Result<Option<Arc<CompositeTypeDescriptor>>>;
81}
82
83pub 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
98pub 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
148fn 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
169pub 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
187fn 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
223pub 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}