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