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