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