Skip to main content

uqa_sql/expr/casting/
mod.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! SQL cast dispatch and scalar, numeric, and range conversion.
8
9mod array;
10mod binary_oid;
11mod legacy_vector;
12mod temporal;
13
14use super::conversion::value_to_string_with_control;
15use super::{out_of_range, ArrayValue, Result, SQLError, TemporalValue, Value};
16use crate::ast::RangeSubtype;
17use uqa_core::memory::{Produced, ProductionControl, ProductionString, ProductionVec};
18
19/// Cast a value to the named SQL type, mirroring `CAST(expr AS ty)`.
20/// Types outside the engine's coercion surface return
21/// [`SQLError::Unsupported`].
22pub fn cast_value(v: &Value, ty: &str) -> Result<Value> {
23    cast_value_from(v, ty, None)
24}
25
26/// Cast a value while preserving an explicitly declared source type when the runtime carrier erases it. `PostgreSQL` 18 integer-to-`bytea`/`oid` casts and `xid` cast rejection require the source's declared identity.
27pub fn cast_value_from(v: &Value, ty: &str, source_ty: Option<&str>) -> Result<Value> {
28    cast_value_from_with_control(v, ty, source_ty, &ProductionControl::uncontrolled())?
29        .into_uncontrolled()
30        .map_err(|_| SQLError::Internal("ordinary cast production owner".into()))
31}
32
33#[expect(
34    clippy::too_many_lines,
35    reason = "cast matrix preserves source-target and error precedence"
36)]
37pub fn cast_value_from_with_control(
38    v: &Value,
39    ty: &str,
40    source_ty: Option<&str>,
41    control: &ProductionControl<'_>,
42) -> Result<Produced<Value>> {
43    control.check()?;
44    if let Value::Datum(datum) = v {
45        return cast_value_from_with_control(
46            &*super::datums::read_with_control(datum, control)?,
47            ty,
48            source_ty,
49            control,
50        );
51    }
52    if let Some(element) = array_scalar_type_name(ty) {
53        let element = element.trim();
54        let element = element
55            .split_once('.')
56            .filter(|(schema, _)| schema.eq_ignore_ascii_case("pg_catalog"))
57            .map_or(element, |(_, name)| name);
58        if let Some(name) = ["void", "pg_node_tree"]
59            .into_iter()
60            .find(|name| element.eq_ignore_ascii_case(name))
61        {
62            return Err(SQLError::Routine {
63                sqlstate: "42704".into(),
64                message: format!("type \"{name}[]\" does not exist"),
65            });
66        }
67    }
68    if matches!(v, Value::Null) {
69        return Ok(control.finish(Value::Null, control.empty_reservation())?);
70    }
71    // Physical cast dispatch consumes the SQL owner's resolved builtin class, including quoted identifiers and catalog interval modifiers.
72    let canonical;
73    let ty = if ty.contains('"') || ty.starts_with("pg_catalog.") {
74        canonical = crate::ColumnType::from_sql_name_with_control(ty, control)?
75            .sql_name_with_control(control)?;
76        canonical.as_str()
77    } else {
78        ty
79    };
80    let canonical_source;
81    let source_ty = match source_ty {
82        Some(source) if source.contains('"') || source.starts_with("pg_catalog.") => {
83            canonical_source = crate::ColumnType::from_sql_name_with_control(source, control)?
84                .sql_name_with_control(control)?;
85            Some(canonical_source.as_str())
86        }
87        source => source,
88    };
89    let (base, modifier) = crate::ast::split_type_modifier_with_control(ty, control)?;
90    let target = base
91        .trim()
92        .strip_prefix("pg_catalog.")
93        .unwrap_or(base.trim());
94    if matches!(v, Value::Void)
95        && !matches!(
96            target,
97            "void"
98                | "text"
99                | "name"
100                | "varchar"
101                | "character varying"
102                | "bpchar"
103                | "character"
104                | "char"
105        )
106    {
107        return Err(undefined_cast("void", postgres_type_display_name(target)));
108    }
109    if let Some(element_type) = array_scalar_type_name(ty) {
110        let source_element_type = source_ty.and_then(array_scalar_type_name).or(match v {
111            Value::LegacyVector(vector) => Some(match vector.kind() {
112                uqa_core::LegacyVectorKind::SmallInteger => "smallint",
113                uqa_core::LegacyVectorKind::Oid => "oid",
114            }),
115            _ => None,
116        });
117        let parsed;
118        let array = match v {
119            Value::Array(array) => array,
120            Value::LegacyVector(vector) => vector.as_array(),
121            Value::Str(text) => {
122                parsed = array::parse_pg_array_literal_with_control(text, control)?;
123                &parsed
124            }
125            other => {
126                return Err(SQLError::TypeMismatch(format!(
127                    "CAST AS {ty}: expected array, got {other:?}"
128                )))
129            }
130        };
131        let elements = array::cast_array_elements(
132            array.elements(),
133            element_type,
134            source_element_type,
135            control,
136        )?;
137        let normalize_empty = array.elements().is_empty()
138            && !array::binary_compatible_elements(source_element_type, element_type, control)?;
139        let mut bounds = ProductionVec::new(*control);
140        bounds.reserve(array.lower_bounds().len())?;
141        if !normalize_empty {
142            for lower in array.lower_bounds() {
143                bounds.push_copy(*lower)?;
144            }
145        }
146        let array =
147            ArrayValue::with_lower_bounds_with_control(elements, bounds.finish()?, control)?
148                .ok_or_else(|| {
149                    SQLError::TypeMismatch("array dimensions changed during cast".into())
150                })?;
151        let (array, memory) = array.into_parts();
152        let element = crate::ColumnType::from_sql_name_with_control(element_type, control)?;
153        let oid = crate::catalog::type_metadata::pg_type_oid(&element) as u32;
154        return Ok(control.finish(Value::Array(array.with_element_type_oid(Some(oid))), memory)?);
155    }
156    let value = match &**base {
157        "void" | "pg_catalog.void" => {
158            let source = canonical_cast_source_with_control(source_ty, v, control)?;
159            if matches!(
160                source.as_str(),
161                "unknown" | "text" | "name" | "varchar" | "bpchar" | "void"
162            ) {
163                Ok(Value::Void)
164            } else {
165                Err(undefined_cast(postgres_type_display_name(&source), "void"))
166            }
167        }
168        "smallint" | "int2" | "pg_catalog.int2" => cast_integer(v, "smallint", control),
169        "integer" | "int" | "int4" | "serial" | "serial4" | "pg_catalog.int4" => {
170            binary_oid::cast_integer_from(v, source_ty, control)
171        }
172        "bigint" | "int8" | "bigserial" | "serial8" | "pg_catalog.int8" => {
173            cast_integer(v, "bigint", control)
174        }
175        "real" | "float4" | "pg_catalog.float4" => {
176            super::floating::to_float_with_control(v, super::FloatWidth::Real, control)
177                .map(Value::Float)
178        }
179        "float8" | "double" | "double precision" | "pg_catalog.float8" => {
180            super::floating::to_float_with_control(v, super::FloatWidth::DoublePrecision, control)
181                .map(Value::Float)
182        }
183        "numeric" | "decimal" => {
184            let value = super::conversion::to_decimal_with_control(v, control)?;
185            let value = if let Some(modifier) = modifier {
186                let mut parts = modifier.split(',').map(str::trim);
187                let precision: u32 = parts
188                    .next()
189                    .and_then(|p| p.parse().ok())
190                    .ok_or_else(|| SQLError::TypeMismatch("bad numeric precision".into()))?;
191                let scale: i32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
192                let rounded = value
193                    .round_to_scale_with_control(scale, control)?
194                    .ok_or_else(|| out_of_range("numeric"))?;
195                if !rounded.fits_precision_with_control(precision, scale, control)? {
196                    return Err(crate::assignment::conversion::numeric_field_overflow(
197                        precision, scale,
198                    ));
199                }
200                rounded
201            } else {
202                value
203            };
204            let (value, memory) = value.into_parts();
205            return Ok(control.finish(Value::Decimal(value), memory)?);
206        }
207        "regproc" | "regprocedure" | "regrole" | "regtype" if matches!(v, Value::Int(_)) => {
208            Ok(v.clone())
209        }
210        "pg_node_tree" => {
211            let source = canonical_cast_source_with_control(source_ty, v, control)?;
212            return match source.as_str() {
213                "pg_node_tree" => Ok(control.copy_value(v)?),
214                "unknown" | "text" | "varchar" | "bpchar" | "name" => Err(SQLError::Unsupported(
215                    "cannot accept a value of type pg_node_tree".into(),
216                )),
217                _ => Err(undefined_cast(
218                    postgres_type_display_name(&source),
219                    "pg_node_tree",
220                )),
221            };
222        }
223        "text"
224        | "refcursor"
225        | "pg_catalog.refcursor"
226        | "name"
227        | "regproc"
228        | "regprocedure"
229        | "regtype"
230        | "aclitem" => {
231            let source = source_ty
232                .map(str::trim)
233                .map(|source| source.strip_prefix("pg_catalog.").unwrap_or(source));
234            let text = match (source, v) {
235                (Some("int2vector" | "oidvector"), _) => {
236                    match super::conversion::vector_value_to_string_with_control(v, control)? {
237                        Some(text) => text,
238                        None => value_to_string_with_control(v, control)?,
239                    }
240                }
241                (
242                    Some(
243                        "regproc" | "regprocedure" | "regclass" | "regcollation" | "regnamespace"
244                        | "regrole" | "regtype",
245                    ),
246                    Value::Int(0),
247                ) => control.copy_text("-")?,
248                _ => cast_text(v, source_ty, control)?,
249            };
250            return text_value(text, false, control);
251        }
252        "int2vector" | "pg_catalog.int2vector" => {
253            return legacy_vector::cast_int2vector(v, source_ty, control)
254        }
255        "oidvector" | "pg_catalog.oidvector" => {
256            return legacy_vector::cast_oidvector(v, source_ty, control)
257        }
258        "oid" | "pg_catalog.oid" => cast_oid(v, source_ty, control),
259        "regclass"
260        | "pg_catalog.regclass"
261        | "regnamespace"
262        | "pg_catalog.regnamespace"
263        | "regcollation"
264        | "pg_catalog.regcollation"
265        | "regrole"
266        | "pg_catalog.regrole" => return cast_named_oid(v, target, source_ty, control),
267        "xid" | "pg_catalog.xid" => cast_xid(v, source_ty, control),
268        "\"char\"" => {
269            let source = canonical_cast_source_with_control(source_ty, v, control)?;
270            let byte = if source.as_str() == "\"char\"" {
271                super::internal_char::value_byte(v)
272                    .ok_or_else(|| SQLError::Internal("invalid internal char carrier".into()))?
273            } else if let ("int4", Value::Int(value)) = (source.as_str(), v) {
274                i8::try_from(*value).map_err(|_| out_of_range("\"char\""))? as u8
275            } else {
276                let text = value_to_string_with_control(v, control)?;
277                super::internal_char::input_byte(&text)
278            };
279            return Ok(super::internal_char::carrier_with_control(byte, control)?);
280        }
281        "uuid" => return cast_uuid(v, control),
282        // Every row is a record; `record_in` cannot read text without a composite type to read it as.
283        "record" => {
284            return match v {
285                Value::Row(_) | Value::Record(_) => Ok(control.copy_value(v)?),
286                Value::Str(_) | Value::FixedChar(_) => Err(SQLError::Routine {
287                    sqlstate: "0A000".into(),
288                    message: "input of anonymous composite types is not implemented".into(),
289                }),
290                _ => Err(undefined_cast(
291                    &canonical_cast_source_with_control(source_ty, v, control)?,
292                    "record",
293                )),
294            }
295        }
296        "varchar" | "character varying" => {
297            let text = cast_text(v, source_ty, control)?;
298            let Some(modifier) = modifier else {
299                return text_value(text, false, control);
300            };
301            let limit: usize = modifier
302                .trim()
303                .parse()
304                .map_err(|_| SQLError::TypeMismatch(format!("bad length modifier {modifier}")))?;
305            return character_value(text, limit, false, control);
306        }
307        "bpchar" if modifier.is_none() => {
308            let text = match v {
309                // A relabel to the same unlimited type keeps its padding;
310                // conversion through text would trim significant enum input.
311                Value::FixedChar(text) => control.copy_text(text)?,
312                _ => cast_text(v, source_ty, control)?,
313            };
314            return text_value(text, true, control);
315        }
316        "character" | "char" | "bpchar" => {
317            let text = cast_text(v, source_ty, control)?;
318            let limit: usize = match modifier {
319                Some(modifier) => modifier.trim().parse().map_err(|_| {
320                    SQLError::TypeMismatch(format!("bad length modifier {modifier}"))
321                })?,
322                None => 1,
323            };
324            if limit == 0 {
325                return Err(SQLError::TypeMismatch(
326                    "CHARACTER length must be greater than zero".into(),
327                ));
328            }
329            return character_value(text, limit, true, control);
330        }
331        "date" => cast_temporal(v, source_ty, TemporalCastTarget::Date, None, control),
332        "time" | "time without time zone" => {
333            cast_temporal(v, source_ty, TemporalCastTarget::Time, modifier, control)
334        }
335        "timetz" | "time with time zone" => {
336            cast_temporal(v, source_ty, TemporalCastTarget::TimeTz, modifier, control)
337        }
338        "timestamp" | "datetime" | "timestamp without time zone" => cast_temporal(
339            v,
340            source_ty,
341            TemporalCastTarget::Timestamp,
342            modifier,
343            control,
344        ),
345        "timestamptz" | "timestamp with time zone" => cast_temporal(
346            v,
347            source_ty,
348            TemporalCastTarget::TimestampTz,
349            modifier,
350            control,
351        ),
352        "interval" => temporal::cast_interval(v, source_ty, ty, control),
353        name if name.starts_with("interval ") => temporal::cast_interval(v, source_ty, ty, control),
354        "int4range" => return cast_range(v, source_ty, RangeSubtype::Integer, control),
355        "int8range" => return cast_range(v, source_ty, RangeSubtype::BigInteger, control),
356        "numrange" => return cast_range(v, source_ty, RangeSubtype::Numeric, control),
357        "daterange" => return cast_range(v, source_ty, RangeSubtype::Date, control),
358        "tsrange" => return cast_range(v, source_ty, RangeSubtype::Timestamp, control),
359        "tstzrange" => return cast_range(v, source_ty, RangeSubtype::TimestampTz, control),
360        "int4multirange" => return cast_multirange(v, source_ty, RangeSubtype::Integer, control),
361        "int8multirange" => {
362            return cast_multirange(v, source_ty, RangeSubtype::BigInteger, control)
363        }
364        "nummultirange" => return cast_multirange(v, source_ty, RangeSubtype::Numeric, control),
365        "datemultirange" => return cast_multirange(v, source_ty, RangeSubtype::Date, control),
366        "tsmultirange" => return cast_multirange(v, source_ty, RangeSubtype::Timestamp, control),
367        "tstzmultirange" => {
368            return cast_multirange(v, source_ty, RangeSubtype::TimestampTz, control)
369        }
370        "json" => return super::json::cast_json_value_with_control(v, false, control),
371        "jsonb" => return super::json::cast_json_value_with_control(v, true, control),
372        "bytea" => return cast_bytea(v, source_ty, control),
373        "boolean" | "bool" => cast_boolean(v),
374        other => Err(SQLError::Unsupported(format!("CAST AS {other}"))),
375    }?;
376    Ok(control.finish(value, control.empty_reservation())?)
377}
378
379/// Repeated array brackets declare dimensions; every scalar leaf uses the same element input type.
380fn array_scalar_type_name(ty: &str) -> Option<&str> {
381    let mut element = ty.trim().strip_suffix("[]")?.trim_end();
382    while let Some(inner) = element.strip_suffix("[]") {
383        element = inner.trim_end();
384    }
385    Some(element)
386}
387
388fn text_value(
389    text: Produced<String>,
390    fixed: bool,
391    control: &ProductionControl<'_>,
392) -> Result<Produced<Value>> {
393    let (text, memory) = text.into_parts();
394    Ok(control.finish(
395        if fixed {
396            Value::FixedChar(text)
397        } else {
398            Value::Str(text)
399        },
400        memory,
401    )?)
402}
403
404fn character_value(
405    text: Produced<String>,
406    limit: usize,
407    fixed: bool,
408    control: &ProductionControl<'_>,
409) -> Result<Produced<Value>> {
410    let mut count = 0;
411    let mut end = 0;
412    for (index, character) in text.char_indices().take(limit) {
413        control.check()?;
414        count += 1;
415        end = index + character.len_utf8();
416    }
417    let mut output = ProductionString::from_produced(text, *control)?;
418    output.truncate(end)?;
419    if fixed {
420        for _ in count..limit {
421            output.push(' ')?;
422        }
423    }
424    text_value(output.finish()?, fixed, control)
425}
426
427fn cast_range(
428    v: &Value,
429    source_ty: Option<&str>,
430    subtype: RangeSubtype,
431    control: &ProductionControl<'_>,
432) -> Result<Produced<Value>> {
433    let source = source_ty
434        .map(|source| canonical_type_name(source, control))
435        .transpose()?;
436    let source = source.as_ref().map(|source| source.as_str());
437    if source.is_some_and(|source| {
438        source != subtype.range_name() && !matches!(source, "unknown" | "cstring")
439    }) {
440        return Err(undefined_cast(
441            source.unwrap_or("unknown"),
442            subtype.range_name(),
443        ));
444    }
445    let (Value::Str(text) | Value::FixedChar(text)) = v else {
446        return Err(undefined_cast(
447            source.unwrap_or("unknown"),
448            subtype.range_name(),
449        ));
450    };
451    text_value(
452        super::range::canonical_range_text_with_control(text, subtype, control)?,
453        false,
454        control,
455    )
456}
457
458fn cast_multirange(
459    v: &Value,
460    source_ty: Option<&str>,
461    subtype: RangeSubtype,
462    control: &ProductionControl<'_>,
463) -> Result<Produced<Value>> {
464    let source = source_ty
465        .map(|source| canonical_type_name(source, control))
466        .transpose()?;
467    let source = source.as_ref().map(|source| source.as_str());
468    let (Value::Str(text) | Value::FixedChar(text)) = v else {
469        return Err(undefined_cast(
470            source.unwrap_or("unknown"),
471            subtype.multirange_name(),
472        ));
473    };
474    let text = match source {
475        Some(source) if source == subtype.range_name() => {
476            super::range::canonical_range_as_multirange_text_with_control(text, subtype, control)?
477        }
478        None | Some("unknown" | "cstring") => {
479            super::range::canonical_multirange_text_with_control(text, subtype, control)?
480        }
481        Some(source) if source == subtype.multirange_name() => {
482            super::range::canonical_multirange_text_with_control(text, subtype, control)?
483        }
484        Some(source) => return Err(undefined_cast(source, subtype.multirange_name())),
485    };
486    text_value(text, false, control)
487}
488
489fn canonical_type_name(
490    type_name: &str,
491    control: &ProductionControl<'_>,
492) -> Result<Produced<String>> {
493    let mut normalized = ProductionString::new(*control);
494    for character in type_name.trim().chars() {
495        normalized.push(character.to_ascii_lowercase())?;
496    }
497    Ok(control.copy_text(
498        normalized
499            .strip_prefix("pg_catalog.")
500            .unwrap_or(&normalized),
501    )?)
502}
503
504/// Apply `PostgreSQL` prefix `-` while retaining the operand's declared type.
505pub fn negate_value(value: &Value, source_ty: Option<&str>) -> Result<Value> {
506    negate_value_with_control(value, source_ty, &ProductionControl::uncontrolled())?
507        .into_uncontrolled()
508        .map_err(|_| SQLError::Internal("ordinary negation owner".into()))
509}
510
511pub fn negate_value_with_control(
512    value: &Value,
513    source_ty: Option<&str>,
514    control: &ProductionControl<'_>,
515) -> Result<Produced<Value>> {
516    control.check()?;
517    if matches!(value, Value::Null) {
518        return Ok(control.finish(Value::Null, control.empty_reservation())?);
519    }
520    let source = canonical_cast_source_with_control(source_ty, value, control)?;
521    let result = match (source.as_str(), value) {
522        ("int2", Value::Int(value)) => i16::try_from(*value)
523            .ok()
524            .and_then(i16::checked_neg)
525            .map(|value| Value::Int(i64::from(value)))
526            .ok_or_else(|| out_of_range("smallint")),
527        ("int4", Value::Int(value)) => i32::try_from(*value)
528            .ok()
529            .and_then(i32::checked_neg)
530            .map(|value| Value::Int(i64::from(value)))
531            .ok_or_else(|| out_of_range("integer")),
532        ("int8", Value::Int(value)) => value
533            .checked_neg()
534            .map(Value::Int)
535            .ok_or_else(|| out_of_range("bigint")),
536        ("float4" | "float8", Value::Float(value)) => Ok(Value::Float(-value)),
537        ("numeric", Value::Decimal(value)) => {
538            let (value, memory) = value.negated_with_control(control)?.into_parts();
539            return Ok(control.finish(Value::Decimal(value), memory)?);
540        }
541        ("interval", Value::Temporal(interval @ TemporalValue::Interval { .. })) => {
542            Ok(Value::Temporal(
543                super::IntervalFields::of(interval)
544                    .expect("interval operand")
545                    .negate()?
546                    .value(),
547            ))
548        }
549        _ => Err(crate::type_resolution::undefined_prefix_operator(
550            "-",
551            postgres_type_display_name(source.as_str()),
552        )),
553    }?;
554    Ok(control.finish(result, control.empty_reservation())?)
555}
556
557fn canonical_cast_source_with_control(
558    source_ty: Option<&str>,
559    value: &Value,
560    control: &ProductionControl<'_>,
561) -> Result<Produced<String>> {
562    let source = source_ty.unwrap_or(match value {
563        Value::Str(_) | Value::FixedChar(_) => "unknown",
564        Value::Int(_) => "integer",
565        Value::Bool(_) => "boolean",
566        Value::Float(_) => "double precision",
567        Value::Decimal(_) => "numeric",
568        Value::Bytes(_) => "bytea",
569        Value::Temporal(TemporalValue::Date { .. }) => "date",
570        Value::Temporal(TemporalValue::Time { .. }) => "time without time zone",
571        Value::Temporal(TemporalValue::TimeTz { .. }) => "time with time zone",
572        Value::Temporal(TemporalValue::Timestamp { .. }) => "timestamp without time zone",
573        Value::Temporal(TemporalValue::TimestampTz { .. }) => "timestamp with time zone",
574        Value::Temporal(TemporalValue::Interval { .. }) => "interval",
575        Value::Json(_) => "json",
576        Value::JsonB(_) => "jsonb",
577        Value::Array(_) => "anyarray",
578        Value::Enum(_) => "anyenum",
579        Value::Datum(value) => super::datums::type_name(value),
580        Value::LegacyVector(vector) => vector.kind().type_name(),
581        Value::List(_) => "anyarray",
582        Value::Row(_) | Value::Record(_) => "record",
583        Value::Map(_) => "jsonb",
584        Value::Null => "unknown",
585        Value::Void => "void",
586    });
587    let declared;
588    let source = if source.contains('"') || source.starts_with("pg_catalog.") {
589        declared = crate::ColumnType::from_sql_name_with_control(source, control)?
590            .sql_name_with_control(control)?;
591        declared.as_str()
592    } else {
593        source
594    };
595    let (source, _) = crate::ast::split_type_modifier_with_control(source, control)?;
596    let mut normalized = ProductionString::new(*control);
597    for (index, word) in source.split_whitespace().enumerate() {
598        if index != 0 {
599            normalized.push(' ')?;
600        }
601        for character in word.chars() {
602            normalized.push(character.to_ascii_lowercase())?;
603        }
604    }
605    let source = normalized
606        .strip_prefix("pg_catalog.")
607        .unwrap_or(&normalized);
608    let canonical = match source {
609        "smallint" | "int2" => "int2",
610        "integer" | "int" | "int4" | "serial" | "serial4" => "int4",
611        "bigint" | "int8" | "bigserial" | "serial8" => "int8",
612        "character varying" | "varchar" => "varchar",
613        "character" | "char" | "bpchar" => "bpchar",
614        "boolean" | "bool" => "bool",
615        "double" | "double precision" | "float8" => "float8",
616        "real" | "float4" => "float4",
617        other => other,
618    };
619    Ok(control.copy_text(canonical)?)
620}
621
622fn undefined_cast(source: &str, target: &str) -> SQLError {
623    SQLError::Routine {
624        sqlstate: "42846".into(),
625        message: format!("cannot cast type {source} to {target}"),
626    }
627}
628
629fn postgres_type_display_name(name: &str) -> &str {
630    match name {
631        "int2" => "smallint",
632        "int4" => "integer",
633        "int8" => "bigint",
634        "float4" => "real",
635        "float8" => "double precision",
636        "bool" => "boolean",
637        "varchar" => "character varying",
638        "bpchar" => "character",
639        other => other,
640    }
641}
642
643fn cast_text(
644    value: &Value,
645    source: Option<&str>,
646    control: &ProductionControl<'_>,
647) -> Result<Produced<String>> {
648    if source == Some("\"char\"") {
649        let byte = super::internal_char::value_byte(value)
650            .ok_or_else(|| SQLError::Internal("invalid internal char carrier".into()))?;
651        return Ok(super::internal_char::output_with_control(byte, control)?);
652    }
653    if let (Value::Float(value), Some(source)) = (value, source) {
654        let source = crate::ast::ColumnType::from_sql_name_with_control(source, control);
655        match source {
656            Ok(source) if matches!(&*source, crate::ast::ColumnType::Real) => {
657                return super::floating::format_real_with_control(*value as f32, control)
658            }
659            Err(error) if matches!(error.sqlstate(), Some("53200" | "57014")) => return Err(error),
660            _ => {}
661        }
662    }
663    value_to_string_with_control(value, control)
664}
665
666fn cast_uuid(value: &Value, control: &ProductionControl<'_>) -> Result<Produced<Value>> {
667    let text = match value {
668        Value::Str(text) | Value::FixedChar(text) => text,
669        other => {
670            return Err(SQLError::TypeMismatch(format!(
671                "cannot cast {other:?} to uuid"
672            )))
673        }
674    };
675    text_value(
676        super::uuid::canonicalize_uuid_with_control(text, control)?,
677        false,
678        control,
679    )
680}
681
682/// CAST to the integer family with `PostgreSQL` conversion rules:
683/// float8 rounds half-to-even, numeric rounds half-away-from-zero,
684/// strings must be integral text, and the result must fit the target
685/// width.
686pub(super) fn cast_integer(
687    v: &Value,
688    target: &str,
689    control: &ProductionControl<'_>,
690) -> Result<Value> {
691    control.check()?;
692    let n: i64 = match v {
693        Value::Int(n) => *n,
694        Value::Bool(b) => i64::from(*b),
695        Value::Float(f) => {
696            if !f.is_finite() {
697                return Err(out_of_range(target));
698            }
699            let rounded = f.round_ties_even();
700            // `i64::MAX as f64` rounds up to 2^63.  Comparing with `>` would
701            // therefore admit 2^63 and Rust's float-to-int cast would silently
702            // saturate it to `i64::MAX`.
703            if rounded < i64::MIN as f64 || rounded >= 9_223_372_036_854_775_808.0 {
704                return Err(out_of_range(target));
705            }
706            rounded as i64
707        }
708        Value::Decimal(d) => d
709            .round_to_scale_with_control(0, control)?
710            .ok_or_else(|| out_of_range(target))?
711            .to_i64_trunc_with_control(control)?
712            .ok_or_else(|| out_of_range(target))?,
713        Value::Str(s) | Value::FixedChar(s) => return integer_from_text(s, target),
714        Value::Bytes(bytes) => bytea_to_integer(bytes, target)?,
715        other => {
716            return Err(SQLError::TypeMismatch(format!(
717                "cannot cast {other:?} to {target}"
718            )));
719        }
720    };
721    let in_range = match target {
722        "smallint" => i16::try_from(n).is_ok(),
723        "integer" => i32::try_from(n).is_ok(),
724        _ => true,
725    };
726    if !in_range {
727        return Err(out_of_range(target));
728    }
729    Ok(Value::Int(n))
730}
731
732/// `boolin`: `PostgreSQL`'s `parse_bool` over trimmed text, accepting case-insensitive prefixes of `true`, `false`, `yes` and `no`, `on`, at least two letters of `off`, and `1` and `0`.
733#[must_use]
734pub fn parse_boolean_input(text: &str) -> Option<bool> {
735    let text = text.trim();
736    let matches_prefix = |word: &str| {
737        !text.is_empty()
738            && word
739                .get(..text.len())
740                .is_some_and(|prefix| prefix.eq_ignore_ascii_case(text))
741    };
742    if matches_prefix("true") || matches_prefix("yes") || text == "1" {
743        Some(true)
744    } else if matches_prefix("false") || matches_prefix("no") || text == "0" {
745        Some(false)
746    } else if text.eq_ignore_ascii_case("on") {
747        Some(true)
748    } else if matches_prefix("off") && text.len() >= 2 {
749        Some(false)
750    } else {
751        None
752    }
753}
754
755/// The error `boolin` reports for text that is not a boolean.
756#[must_use]
757pub fn invalid_boolean_input(text: &str) -> SQLError {
758    SQLError::Routine {
759        sqlstate: "22P02".into(),
760        message: format!("invalid input syntax for type boolean: \"{text}\""),
761    }
762}
763
764/// Text read as `PostgreSQL`'s `int2in`, `int4in` and `int8in` read it, whose out-of-range error names the text and the type.
765fn integer_from_text(text: &str, target: &str) -> Result<Value> {
766    use crate::expr::integer_input::{parse_int8, IntegerInputError};
767    let out_of_range = || SQLError::Routine {
768        sqlstate: "22003".into(),
769        message: format!("value \"{text}\" is out of range for type {target}"),
770    };
771    let value = parse_int8(text).map_err(|error| match error {
772        IntegerInputError::OutOfRange => out_of_range(),
773        IntegerInputError::InvalidSyntax => SQLError::Routine {
774            sqlstate: "22P02".into(),
775            message: format!("invalid input syntax for type {target}: \"{text}\""),
776        },
777    })?;
778    let in_range = match target {
779        "smallint" => i16::try_from(value).is_ok(),
780        "integer" => i32::try_from(value).is_ok(),
781        _ => true,
782    };
783    if in_range {
784        Ok(Value::Int(value))
785    } else {
786        Err(out_of_range())
787    }
788}
789
790/// CAST to boolean: strings follow `PostgreSQL`'s `parse_bool`
791/// (prefixes of true/false/yes/no, on/off, 1/0); numbers are non-zero
792/// tests.
793pub(super) fn cast_boolean(v: &Value) -> Result<Value> {
794    match v {
795        Value::Bool(b) => Ok(Value::Bool(*b)),
796        Value::Int(n) => Ok(Value::Bool(*n != 0)),
797        Value::Float(f) => Ok(Value::Bool(*f != 0.0)),
798        Value::Decimal(d) => Ok(Value::Bool(!d.is_zero())),
799        Value::Str(s) | Value::FixedChar(s) => parse_boolean_input(s)
800            .map(Value::Bool)
801            .ok_or_else(|| invalid_boolean_input(s)),
802        other => Err(SQLError::TypeMismatch(format!(
803            "cannot cast {other:?} to boolean"
804        ))),
805    }
806}
807
808pub use array::{array_dimensions, parse_pg_array_literal, parse_pg_array_literal_with_control};
809use binary_oid::{bytea_to_integer, cast_bytea, cast_named_oid, cast_oid, cast_xid};
810use temporal::{cast_temporal, TemporalCastTarget};
811
812#[cfg(test)]
813mod production_tests;
814#[cfg(test)]
815mod tests;