1use uqa_core::{
10 memory::{Produced, ProductionControl, ProductionString, ProductionVec},
11 ArrayValue, Value,
12};
13
14use super::super::casting::{cast_value_from_with_control, parse_pg_array_literal_with_control};
15use super::{format_regtype_elements_with_control, format_regtype_value_with_control, EngineHook};
16use crate::{
17 ast::ColumnType,
18 error::{Result, SQLError},
19};
20
21mod datestyle;
22mod timezone;
23
24pub trait CatalogInputFunctions {
26 fn read_unknown_input(&self, text: &str, target: &ColumnType) -> Result<Value>;
27}
28
29pub(crate) fn requires_domain_array_input(target: &ColumnType) -> bool {
30 matches!(target, ColumnType::Array(element) if matches!(array_leaf_type(element), ColumnType::Domain { .. }))
31}
32
33pub(crate) fn requires_catalog_constant_input(target: &ColumnType) -> bool {
35 requires_domain_array_input(target)
36 || match target {
37 ColumnType::Composite(_) => true,
38 ColumnType::Array(element) | ColumnType::Domain { base: element, .. } => {
39 requires_catalog_constant_input(element)
40 }
41 _ => false,
42 }
43}
44
45pub fn read_catalog_input(
47 text: &str,
48 target: &ColumnType,
49 engine: &dyn EngineHook,
50) -> Result<Value> {
51 if requires_domain_array_input(target) {
52 return read_catalog_array_input(text, target, engine);
53 }
54 if !requires_catalog_constant_input(target) {
55 return Err(SQLError::Internal(
56 "catalog input requires a composite or domain array".into(),
57 ));
58 }
59 super::cast_value_with_type_resolution(
60 &Value::Str(text.into()),
61 Some("unknown"),
62 &target.catalog_name(),
63 Some(engine),
64 )
65}
66
67pub fn read_catalog_array_input(
69 text: &str,
70 target: &ColumnType,
71 engine: &dyn EngineHook,
72) -> Result<Value> {
73 if !requires_domain_array_input(target) {
74 return Err(SQLError::Internal(
75 "catalog array input requires domain elements".into(),
76 ));
77 }
78 let control = ProductionControl::uncontrolled();
79 let parsed = parse_pg_array_literal_with_control(text, &control)?
80 .into_uncontrolled()
81 .map_err(|_| SQLError::Internal("ordinary catalog array input owner".into()))?;
82 cast_catalog_array(&Value::Array(parsed), None, target, engine, &control)?
83 .into_uncontrolled()
84 .map_err(|_| SQLError::Internal("ordinary catalog array input owner".into()))
85}
86
87#[must_use]
88pub fn coercion_type_name(ty: &ColumnType) -> String {
89 coercion_type_name_with_control(ty, &ProductionControl::uncontrolled())
90 .expect("ordinary coercion type name")
91 .into_uncontrolled()
92 .expect("ordinary coercion type name owner")
93}
94
95fn coercion_type_name_with_control(
96 ty: &ColumnType,
97 control: &ProductionControl<'_>,
98) -> Result<Produced<String>> {
99 control.check()?;
100 match ty {
101 ColumnType::Domain { base, .. } => coercion_type_name_with_control(base, control),
102 ColumnType::Array(element) => {
103 let element = coercion_type_name_with_control(element, control)?;
104 let mut name = ProductionString::new(*control);
105 name.push_str(&element)?;
106 name.push_str("[]")?;
107 Ok(name.finish()?)
108 }
109 _ => Ok(ty.sql_name_with_control(control)?),
110 }
111}
112
113fn is_string_type(ty: &ColumnType) -> bool {
115 matches!(
116 ty,
117 ColumnType::Text | ColumnType::Name | ColumnType::Varchar(_) | ColumnType::Character(_)
118 )
119}
120
121fn regrole_array_type(ty: &ColumnType) -> bool {
122 match ty {
123 ColumnType::Array(element) => {
124 matches!(element.as_ref(), ColumnType::Regrole) || regrole_array_type(element)
125 }
126 _ => false,
127 }
128}
129
130fn array_leaf_type(ty: &ColumnType) -> &ColumnType {
131 match ty {
132 ColumnType::Array(element) => array_leaf_type(element),
133 _ => ty,
134 }
135}
136
137fn optional_type_name(
138 name: &str,
139 control: &ProductionControl<'_>,
140) -> Result<Option<Produced<ColumnType>>> {
141 match ColumnType::from_sql_name_with_control(name, control) {
142 Ok(ty) => Ok(Some(ty)),
143 Err(error) if matches!(error.sqlstate(), Some("53200" | "57014")) => Err(error),
144 Err(_) => Ok(None),
145 }
146}
147
148pub fn cast_value_with_type_resolution(
150 value: &Value,
151 source_ty: Option<&str>,
152 target_ty: &str,
153 engine: Option<&dyn EngineHook>,
154) -> Result<Value> {
155 cast_value_with_type_resolution_with_control(
156 value,
157 source_ty,
158 target_ty,
159 engine,
160 &ProductionControl::uncontrolled(),
161 )?
162 .into_uncontrolled()
163 .map_err(|_| SQLError::Internal("ordinary catalog cast owner".into()))
164}
165
166fn output_for_cast<'a>(
168 engine: &dyn EngineHook,
169 value: &Value,
170 source_ty: Option<&'a str>,
171 resolved_source_type: Option<&ColumnType>,
172 target: &ColumnType,
173) -> Result<Option<(Value, Option<&'a str>)>> {
174 if let Value::Enum(label) = value {
175 let output =
176 super::super::enums::enum_output_for_cast(engine.enum_labels(), label, target)?;
177 return Ok(Some((output, Some("text"))));
178 }
179 if let Value::Record(record) = value {
180 if let Some(oid) = record.type_oid().filter(|_| is_string_type(target)) {
181 let ty = engine
182 .resolve_type_oid(oid)
183 .map_err(SQLError::Internal)?
184 .ok_or_else(|| SQLError::Routine {
185 sqlstate: "42704".into(),
186 message: format!("type with OID {oid} does not exist"),
187 })?;
188 return Ok(Some((
189 Value::Str(crate::result::format_postgres_text(
190 value,
191 &ty,
192 Some(engine),
193 )?),
194 Some("text"),
195 )));
196 }
197 }
198 if let (Value::Record(_), Some(source @ ColumnType::Composite(_))) =
200 (value, resolved_source_type)
201 {
202 if is_string_type(target) {
203 let text = crate::result::format_postgres_text(value, source, Some(engine))?;
204 return Ok(Some((Value::Str(text), Some("text"))));
205 }
206 }
207 if matches!(value, Value::Record(_) | Value::Row(_))
208 && is_string_type(target)
209 && super::super::enums::contains_enum_carrier(value)
210 {
211 let rendered = super::super::enums::render_enum_labels(engine.enum_labels(), value)?;
212 return Ok(Some((rendered, source_ty)));
213 }
214 Ok(None)
215}
216
217fn catalog_array_output(
219 value: &Value,
220 source_ty: Option<&str>,
221 target_ty: &str,
222 target: &ColumnType,
223 engine: &dyn EngineHook,
224 control: &ProductionControl<'_>,
225) -> Result<Option<Produced<Value>>> {
226 if !matches!(value, Value::Array(_))
227 || !is_string_type(target)
228 || !(super::super::enums::contains_enum_carrier(value)
229 || super::super::datums::contains_datum(value))
230 {
231 return Ok(None);
232 }
233 let actual_source;
234 let source_ty = if let Value::Array(array) = value {
235 actual_source = array
236 .element_type_oid()
237 .map(|oid| engine.resolve_type_oid(oid))
238 .transpose()
239 .map_err(SQLError::Internal)?
240 .flatten()
241 .map(|element| control.copy_text(&ColumnType::Array(Box::new(element)).catalog_name()))
242 .transpose()?;
243 actual_source.as_deref().map(String::as_str).or(source_ty)
244 } else {
245 source_ty
246 };
247 let text_array = ColumnType::Array(Box::new(ColumnType::Text));
248 let labels = cast_catalog_array(value, source_ty, &text_array, engine, control)?;
249 cast_value_from_with_control(&labels, target_ty, Some("text[]"), control).map(Some)
250}
251
252fn physical_output_cast(
253 value: &Value,
254 source_ty: Option<&str>,
255 target_ty: &str,
256 target: &ColumnType,
257 engine: &dyn EngineHook,
258 control: &ProductionControl<'_>,
259) -> Result<Option<Produced<Value>>> {
260 let Value::Datum(datum) = value else {
261 return Ok(None);
262 };
263 if !is_string_type(target) {
264 return Ok(None);
265 }
266 let decoded =
267 super::super::datums::read_with_catalog_and_control(datum, Some(engine), control)?;
268 cast_value_with_type_resolution_with_control(
269 &decoded,
270 source_ty,
271 target_ty,
272 Some(engine),
273 control,
274 )
275 .map(Some)
276}
277
278pub fn cast_value_with_type_resolution_with_control(
280 value: &Value,
281 source_ty: Option<&str>,
282 target_ty: &str,
283 engine: Option<&dyn EngineHook>,
284 control: &ProductionControl<'_>,
285) -> Result<Produced<Value>> {
286 control.check()?;
287 let resolved_target = resolve_catalog_type_with_control(target_ty, engine, control)?;
288 control.check()?;
289 let resolved_source_type = source_ty
290 .map(|source| resolve_catalog_type_with_control(source, engine, control))
291 .transpose()?
292 .flatten();
293 let parsed_target = if resolved_target.is_some() {
294 None
295 } else {
296 optional_type_name(target_ty, control)?
297 };
298 let target_column_type = resolved_target.as_deref().or(parsed_target.as_deref());
299 let _date_order = datestyle::input_scope(value, target_column_type, engine, control)?;
300 if let (Some(engine), Some(target)) = (engine, resolved_target.as_deref()) {
301 if let Some(value) = record_identity_cast(
302 value,
303 resolved_source_type.as_deref(),
304 target,
305 engine,
306 control,
307 )? {
308 return Ok(value);
309 }
310 if let Some(output) =
311 physical_output_cast(value, source_ty, target_ty, target, engine, control)?
312 {
313 return Ok(output);
314 }
315 if let Some(value) = engine.cast_domain(value, source_ty, target)? {
316 return Ok(control.retain_external_value(value)?);
317 }
318 if let Some(value) = super::super::enums::cast_to_enum(
319 engine.enum_labels(),
320 value,
321 resolved_source_type.as_deref(),
322 target,
323 )? {
324 return Ok(control.retain_external_value(value)?);
325 }
326 if let Some(value) = super::super::composites::cast_to_composite(
327 engine,
328 value,
329 resolved_source_type.as_deref(),
330 target,
331 )? {
332 return Ok(control.retain_external_value(value)?);
333 }
334 if let Some(output) =
335 catalog_array_output(value, source_ty, target_ty, target, engine, control)?
336 {
337 return Ok(output);
338 }
339 if let Some((output, output_source)) = output_for_cast(
340 engine,
341 value,
342 source_ty,
343 resolved_source_type.as_deref(),
344 target,
345 )? {
346 return cast_value_with_type_resolution_with_control(
347 &output,
348 output_source,
349 target_ty,
350 Some(engine),
351 control,
352 );
353 }
354 control.check()?;
355 if matches!(target, ColumnType::Array(_))
356 && (requires_catalog_array_cast(target)
357 || super::super::enums::contains_enum_carrier(value)
358 || resolved_source_type
359 .as_deref()
360 .is_some_and(super::super::enums::is_enum_bearing))
361 {
362 return cast_catalog_array(value, source_ty, target, engine, control);
363 }
364 }
365 let resolved_source = resolved_source_type
366 .map(|ty| coercion_type_name_with_control(&ty, control))
367 .transpose()?;
368 let source_ty = resolved_source.as_deref().map(String::as_str).or(source_ty);
369 let target_name = resolved_target
370 .as_ref()
371 .map(|ty| coercion_type_name_with_control(ty, control))
372 .transpose()?;
373 let target_ty = target_name.as_ref().map_or(target_ty, |name| name.as_str());
374 cast_resolved_value(
375 value,
376 source_ty,
377 target_ty,
378 target_column_type,
379 engine,
380 control,
381 )
382}
383
384fn resolve_catalog_type_with_control(
385 name: &str,
386 engine: Option<&dyn EngineHook>,
387 control: &ProductionControl<'_>,
388) -> Result<Option<Produced<ColumnType>>> {
389 engine
390 .map(|engine| engine.resolve_type_name(name))
391 .transpose()
392 .map_err(SQLError::Internal)?
393 .flatten()
394 .map(|ty| {
395 ty.retain_external_with_control(control)
396 .map_err(SQLError::from)
397 })
398 .transpose()
399}
400
401fn record_identity_cast(
402 value: &Value,
403 resolved_source_type: Option<&ColumnType>,
404 target: &ColumnType,
405 engine: &dyn EngineHook,
406 control: &ProductionControl<'_>,
407) -> Result<Option<Produced<Value>>> {
408 if matches!(target, ColumnType::Record) {
409 if let Value::Datum(datum) = value {
411 let ty = engine
412 .resolve_type_oid(datum.type_oid())
413 .map_err(SQLError::Internal)?;
414 if matches!(ty, Some(ColumnType::Composite(_))) {
415 return Ok(Some(control.copy_value(value)?));
416 }
417 }
418 if let (Value::Record(record), Some(ColumnType::Composite(source))) =
419 (value, resolved_source_type)
420 {
421 let (value, memory) = control.copy_value(value)?.into_parts();
422 let Value::Record(value) = value else {
423 unreachable!();
424 };
425 return Ok(Some(control.finish(
426 Value::Record(value.with_type_oid(Some(record.type_oid().unwrap_or(source.oid)))),
427 memory,
428 )?));
429 }
430 }
431
432 Ok(None)
433}
434
435fn cast_resolved_value(
436 value: &Value,
437 source_ty: Option<&str>,
438 target_ty: &str,
439 target_column_type: Option<&ColumnType>,
440 engine: Option<&dyn EngineHook>,
441 control: &ProductionControl<'_>,
442) -> Result<Produced<Value>> {
443 if target_column_type.is_some_and(regrole_array_type) {
444 let source_column_type = source_ty
445 .map(|name| optional_type_name(name, control))
446 .transpose()?
447 .flatten();
448 let source_name = source_column_type
449 .as_deref()
450 .map(array_leaf_type)
451 .map(|ty| ty.sql_name_with_control(control))
452 .transpose()?;
453 return cast_array(
454 value,
455 source_name.as_ref().map(|name| name.as_str()),
456 "regrole",
457 "regrole[]",
458 engine,
459 control,
460 );
461 }
462 if let Some(ColumnType::Array(element)) = target_column_type {
464 if is_string_type(element) {
465 if let Some(source_ty) = source_ty
466 .map(|source| optional_type_name(source, control))
467 .transpose()?
468 .flatten()
469 {
470 if let Some(elements) =
471 format_regtype_elements_with_control(value, &source_ty, engine, control)?
472 {
473 let (elements, _memory) = elements.into_parts();
474 return cast_value_from_with_control(
475 &Value::Array(elements),
476 target_ty,
477 Some("text[]"),
478 control,
479 );
480 }
481 }
482 }
483 }
484 let text_target = target_ty.eq_ignore_ascii_case("text");
485 if text_target || target_column_type.is_some_and(is_string_type) {
486 if let Some(source_ty) = source_ty
487 .map(|source| optional_type_name(source, control))
488 .transpose()?
489 .flatten()
490 {
491 if let Some(text) =
492 format_regtype_value_with_control(value, &source_ty, engine, control)?
493 {
494 let (text, memory) = text.into_parts();
495 if text_target {
496 return Ok(control.finish(Value::Str(text), memory)?);
497 }
498 return cast_value_from_with_control(
499 &Value::Str(text),
500 target_ty,
501 Some("text"),
502 control,
503 );
504 }
505 }
506 }
507 if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
508 let oid = resolve_regobject_input(name, target_ty, target_column_type, engine, control)?;
509 if let Some(oid) = oid {
510 return Ok(control.finish(Value::Int(oid), control.empty_reservation())?);
511 }
512 }
513 let zoned = timezone::cast_local_timestamp(value, target_column_type, engine, control)?;
514 cast_value_from_with_control(
515 zoned.as_ref().unwrap_or(value),
516 target_ty,
517 source_ty,
518 control,
519 )
520}
521
522fn resolve_regobject_input(
523 name: &str,
524 target_ty: &str,
525 target_column_type: Option<&ColumnType>,
526 engine: &dyn EngineHook,
527 control: &ProductionControl<'_>,
528) -> Result<Option<i64>> {
529 enum ObjectKind {
530 Relation,
531 RoutineName,
532 Routine,
533 Role,
534 Namespace,
535 Collation,
536 Type,
537 }
538 let kind = if target_ty.eq_ignore_ascii_case("regclass") {
539 ObjectKind::Relation
540 } else if matches!(target_column_type, Some(ColumnType::Regproc))
541 || target_ty.eq_ignore_ascii_case("regproc")
542 {
543 ObjectKind::RoutineName
544 } else if target_ty.eq_ignore_ascii_case("regprocedure") {
545 ObjectKind::Routine
546 } else if target_ty.eq_ignore_ascii_case("regrole") {
547 ObjectKind::Role
548 } else if matches!(target_column_type, Some(ColumnType::Regnamespace)) {
549 ObjectKind::Namespace
550 } else if matches!(target_column_type, Some(ColumnType::Regcollation)) {
551 ObjectKind::Collation
552 } else if matches!(target_column_type, Some(ColumnType::Regtype)) {
553 ObjectKind::Type
554 } else {
555 return Ok(None);
556 };
557 let oid = match kind {
558 ObjectKind::Relation => engine.resolve_regclass_input(name)?,
559 ObjectKind::RoutineName => return engine.resolve_regproc(name),
561 ObjectKind::Routine => engine.resolve_regprocedure_input(name)?,
562 ObjectKind::Role => engine.resolve_regrole(name)?,
563 ObjectKind::Namespace => engine.resolve_regnamespace(name)?,
564 ObjectKind::Collation => engine.resolve_regcollation(name)?,
565 ObjectKind::Type => engine.resolve_regtype_input(name)?,
566 };
567 control.check()?;
568 if oid.is_some() || matches!(kind, ObjectKind::Type) {
569 return Ok(oid);
570 }
571 let (sqlstate, message) = match kind {
572 ObjectKind::Relation => ("42P01", format!("relation \"{name}\" does not exist")),
573 ObjectKind::Routine => ("42883", format!("function \"{name}\" does not exist")),
574 ObjectKind::Role => ("42704", format!("role \"{name}\" does not exist")),
575 ObjectKind::Namespace => ("3F000", format!("schema \"{name}\" does not exist")),
576 ObjectKind::Collation => (
577 "42704",
578 format!("collation \"{name}\" for encoding \"UTF8\" does not exist"),
579 ),
580 ObjectKind::Type | ObjectKind::RoutineName => {
581 unreachable!("regtype and regproc input return before reporting a missing object")
582 }
583 };
584 Err(SQLError::Routine {
585 sqlstate: sqlstate.into(),
586 message,
587 })
588}
589
590fn requires_catalog_array_cast(ty: &ColumnType) -> bool {
592 match ty {
593 ColumnType::Domain { .. }
594 | ColumnType::Regtype
595 | ColumnType::Regclass
596 | ColumnType::Regproc
597 | ColumnType::Regprocedure
598 | ColumnType::Regcollation
599 | ColumnType::Regnamespace
600 | ColumnType::Enum(_)
601 | ColumnType::Composite(_)
602 | ColumnType::TimestampTz
603 | ColumnType::TimestampTzPrecision(_) => true,
604 ColumnType::Array(element) => requires_catalog_array_cast(element),
605 _ => false,
606 }
607}
608
609fn cast_catalog_array(
610 value: &Value,
611 source: Option<&str>,
612 target: &ColumnType,
613 engine: &dyn EngineHook,
614 control: &ProductionControl<'_>,
615) -> Result<Produced<Value>> {
616 if matches!(value, Value::Null) {
617 return Ok(control.finish(Value::Null, control.empty_reservation())?);
618 }
619 let source_element = source.map(|name| name.trim_end_matches("[]"));
620 let leaf = array_leaf_type(target);
621 let target_element = control.copy_text(&leaf.catalog_name())?;
622 let target_name = target.sql_name_with_control(control)?;
623 let output = cast_array(
624 value,
625 source_element,
626 &target_element,
627 &target_name,
628 Some(engine),
629 control,
630 )?;
631 let (Value::Array(array), memory) = output.into_parts() else {
632 return Err(SQLError::Internal(
633 "catalog array cast returned a non-array".into(),
634 ));
635 };
636 let oid = crate::catalog::type_metadata::pg_type_oid(leaf) as u32;
637 Ok(control.finish(Value::Array(array.with_element_type_oid(Some(oid))), memory)?)
638}
639
640fn cast_array(
641 value: &Value,
642 source: Option<&str>,
643 target_element: &str,
644 target_name: &str,
645 engine: Option<&dyn EngineHook>,
646 control: &ProductionControl<'_>,
647) -> Result<Produced<Value>> {
648 let parsed;
649 let array = match value {
650 Value::Array(array) => array,
651 Value::Str(text) => {
652 parsed = parse_pg_array_literal_with_control(text, control)?;
653 &parsed
654 }
655 other => {
656 return Err(SQLError::TypeMismatch(format!(
657 "CAST AS {target_name}: expected array, got {other:?}"
658 )))
659 }
660 };
661 let elements = cast_array_elements(array.elements(), source, target_element, engine, control)?;
662 let array = rebuild_array(array, elements, control)?
663 .ok_or_else(|| SQLError::TypeMismatch("array dimensions changed during cast".into()))?;
664 let (array, memory) = array.into_parts();
665 let oid = if target_element == "regrole" {
666 Some(crate::catalog::type_metadata::pg_type_oid(&ColumnType::Regrole) as u32)
667 } else {
668 array.element_type_oid()
669 };
670 Ok(control.finish(Value::Array(array.with_element_type_oid(oid)), memory)?)
671}
672
673fn cast_array_elements(
674 values: &[Value],
675 source: Option<&str>,
676 target: &str,
677 engine: Option<&dyn EngineHook>,
678 control: &ProductionControl<'_>,
679) -> Result<Produced<Vec<Value>>> {
680 let mut output = ProductionVec::new(*control);
681 output.reserve(values.len())?;
682 for value in values {
683 let value = match value {
684 Value::List(values) => {
685 let (values, memory) =
686 cast_array_elements(values, source, target, engine, control)?.into_parts();
687 control.finish(Value::List(values), memory)?
688 }
689 value => cast_value_with_type_resolution_with_control(
690 value, source, target, engine, control,
691 )?,
692 };
693 output.push_produced(value)?;
694 }
695 Ok(output.finish()?)
696}
697
698pub(super) fn rebuild_array(
699 source: &ArrayValue,
700 elements: Produced<Vec<Value>>,
701 control: &ProductionControl<'_>,
702) -> Result<Option<Produced<ArrayValue>>> {
703 let mut bounds = ProductionVec::new(*control);
704 bounds.reserve(source.lower_bounds().len())?;
705 for bound in source.lower_bounds() {
706 bounds.push_copy(*bound)?;
707 }
708 ArrayValue::with_lower_bounds_with_control(elements, bounds.finish()?, control)?
709 .map(|array| {
710 let (array, memory) = array.into_parts();
711 control
712 .finish(
713 array.with_element_type_oid(source.element_type_oid()),
714 memory,
715 )
716 .map_err(Into::into)
717 })
718 .transpose()
719}
720
721#[cfg(test)]
722mod tests;