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