1use 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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
34pub struct CompositeAttribute {
35 pub name: String,
36 pub ty: ColumnType,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub collation_oid: Option<u32>,
40 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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
58pub struct CompositeTypeDescriptor {
59 #[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#[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 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
104pub trait CompositeTypeCatalog {
106 fn composite_type(&self, type_oid: u32) -> Result<Option<Arc<CompositeTypeDescriptor>>>;
108}
109
110pub 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
125pub 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
175fn 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
196pub 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
214fn 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
275pub 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}