Skip to main content

uqa_sql/expr/
casting.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! SQL casts and `PostgreSQL` array-literal parsing.
8
9use super::{
10    out_of_range, parse_json, to_decimal, to_f64, typed_json_value, value_to_json, value_to_string,
11    ArrayValue, Result, SQLError, TemporalValue, Value,
12};
13
14/// Cast a value to the named SQL type, mirroring `CAST(expr AS ty)`.
15/// Types outside the engine's coercion surface return
16/// [`SQLError::Unsupported`].
17pub fn cast_value(v: &Value, ty: &str) -> Result<Value> {
18    cast_value_from(v, ty, None)
19}
20
21/// 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.
22pub fn cast_value_from(v: &Value, ty: &str, source_ty: Option<&str>) -> Result<Value> {
23    if matches!(v, Value::Null) {
24        return Ok(Value::Null);
25    }
26    if let Some(elem_ty) = ty.strip_suffix("[]") {
27        let source_elem_ty = source_ty
28            .and_then(|source| source.trim().strip_suffix("[]"))
29            .map(str::trim);
30        let array = match v {
31            Value::Array(array) => array.clone(),
32            Value::Str(s) => parse_pg_array_literal(s)?,
33            other => {
34                return Err(SQLError::TypeMismatch(format!(
35                    "CAST AS {ty}: expected array, got {other:?}"
36                )));
37            }
38        };
39        let elements = cast_array_elements(array.elements(), elem_ty, source_elem_ty)?;
40        return ArrayValue::with_lower_bounds(elements, array.lower_bounds().to_vec())
41            .map(Value::Array)
42            .ok_or_else(|| SQLError::TypeMismatch("array dimensions changed during cast".into()));
43    }
44    let (base, modifier) = split_type_modifier(ty);
45    match base {
46        "smallint" | "int2" | "pg_catalog.int2" => cast_integer(v, "smallint"),
47        "integer" | "int" | "int4" | "serial" | "serial4" | "pg_catalog.int4" => {
48            cast_integer(v, "integer")
49        }
50        "bigint" | "int8" | "bigserial" | "serial8" | "pg_catalog.int8" => {
51            cast_integer(v, "bigint")
52        }
53        "real" | "float4" | "float8" | "double" | "double precision" => {
54            Ok(Value::Float(to_f64(v)?))
55        }
56        "numeric" | "decimal" => {
57            let value = to_decimal(v)?;
58            if let Some(modifier) = modifier {
59                let mut parts = modifier.split(',').map(str::trim);
60                let precision: u32 = parts
61                    .next()
62                    .and_then(|p| p.parse().ok())
63                    .ok_or_else(|| SQLError::TypeMismatch("bad numeric precision".into()))?;
64                let scale: i32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
65                let rounded = value
66                    .round_to_scale(scale)
67                    .ok_or_else(|| out_of_range("numeric"))?;
68                if !rounded.fits_precision(precision, scale) {
69                    return Err(SQLError::Routine {
70                        sqlstate: "22003".into(),
71                        message: format!(
72                            "numeric field overflow: A field with precision {precision}, scale {scale} cannot hold value {}",
73                            value.to_sql_string()
74                        ),
75                    });
76                }
77                return Ok(Value::Decimal(rounded));
78            }
79            Ok(Value::Decimal(value))
80        }
81        "text" | "name" | "regproc" | "regtype" | "pg_node_tree" | "aclitem" => {
82            Ok(Value::Str(value_to_string(v)))
83        }
84        "oid" | "pg_catalog.oid" => cast_oid(v, source_ty),
85        "regclass" | "pg_catalog.regclass" => cast_regclass(v, source_ty),
86        "regnamespace" | "pg_catalog.regnamespace" => cast_regnamespace(v, source_ty),
87        "xid" | "pg_catalog.xid" => cast_xid(v, source_ty),
88        "\"char\"" => {
89            let text = value_to_string(v);
90            let mut characters = text.chars();
91            let Some(character) = characters.next() else {
92                return Ok(Value::Str(String::new()));
93            };
94            if characters.next().is_some() || !character.is_ascii() {
95                return Err(SQLError::TypeMismatch(format!(
96                    "value too long for type character(1): {text:?}"
97                )));
98            }
99            Ok(Value::Str(character.to_string()))
100        }
101        "uuid" => cast_uuid(v),
102        // varchar(n): an explicit cast truncates to the declared length.
103        "varchar" | "character varying" => {
104            let text = value_to_string(v);
105            let Some(modifier) = modifier else {
106                return Ok(Value::Str(text));
107            };
108            let limit: usize = modifier
109                .trim()
110                .parse()
111                .map_err(|_| SQLError::TypeMismatch(format!("bad length modifier {modifier}")))?;
112            Ok(Value::Str(text.chars().take(limit).collect()))
113        }
114        // bpchar is physically blank-padded. Its implicit text coercion strips
115        // those spaces, while a direct result retains them.
116        "bpchar" if modifier.is_none() => Ok(Value::FixedChar(value_to_string(v))),
117        "character" | "char" | "bpchar" => {
118            let text = value_to_string(v);
119            let limit: usize = match modifier {
120                Some(modifier) => modifier.trim().parse().map_err(|_| {
121                    SQLError::TypeMismatch(format!("bad length modifier {modifier}"))
122                })?,
123                None => 1,
124            };
125            if limit == 0 {
126                return Err(SQLError::TypeMismatch(
127                    "CHARACTER length must be greater than zero".into(),
128                ));
129            }
130            let mut text = text.chars().take(limit).collect::<String>();
131            text.extend(std::iter::repeat_n(
132                ' ',
133                limit.saturating_sub(text.chars().count()),
134            ));
135            Ok(Value::FixedChar(text))
136        }
137        "date" => cast_date(v, source_ty),
138        "time" | "time without time zone" => cast_temporal(
139            v,
140            TemporalCastTarget::Time,
141            TemporalValue::parse_time,
142            "time",
143        ),
144        "timetz" | "time with time zone" => cast_temporal(
145            v,
146            TemporalCastTarget::TimeTz,
147            TemporalValue::parse_time_tz,
148            "time with time zone",
149        ),
150        "timestamp" | "datetime" | "timestamp without time zone" => cast_temporal(
151            v,
152            TemporalCastTarget::Timestamp,
153            TemporalValue::parse_timestamp,
154            "timestamp",
155        ),
156        "timestamptz" | "timestamp with time zone" => cast_temporal(
157            v,
158            TemporalCastTarget::TimestampTz,
159            TemporalValue::parse_timestamp_tz,
160            "timestamp with time zone",
161        ),
162        "interval" => cast_temporal(
163            v,
164            TemporalCastTarget::Interval,
165            TemporalValue::parse_interval,
166            "interval",
167        ),
168        "json" => {
169            if let Value::Json(text) = v {
170                return Ok(Value::Json(text.clone()));
171            }
172            if let Value::Str(text) | Value::FixedChar(text) = v {
173                let _validated = parse_json(text)?;
174                return Ok(Value::Json(text.clone()));
175            }
176            typed_json_value(&value_to_json(v), false)
177        }
178        "jsonb" => {
179            if let Value::JsonB(text) = v {
180                return Ok(Value::JsonB(text.clone()));
181            }
182            let parsed = match v {
183                Value::Json(text) | Value::Str(text) | Value::FixedChar(text) => parse_json(text)?,
184                other => value_to_json(other),
185            };
186            typed_json_value(&parsed, true)
187        }
188        "bytea" => cast_bytea(v, source_ty),
189        "boolean" | "bool" => cast_boolean(v),
190        other => Err(SQLError::Unsupported(format!("CAST AS {other}"))),
191    }
192}
193
194/// Apply `PostgreSQL` prefix `-` while retaining the operand's declared type.
195pub fn negate_value(value: &Value, source_ty: Option<&str>) -> Result<Value> {
196    if matches!(value, Value::Null) {
197        return Ok(Value::Null);
198    }
199    let source = canonical_cast_source(source_ty, value);
200    match (source.as_str(), value) {
201        ("int2", Value::Int(value)) => i16::try_from(*value)
202            .ok()
203            .and_then(i16::checked_neg)
204            .map(|value| Value::Int(i64::from(value)))
205            .ok_or_else(|| out_of_range("smallint")),
206        ("int4", Value::Int(value)) => i32::try_from(*value)
207            .ok()
208            .and_then(i32::checked_neg)
209            .map(|value| Value::Int(i64::from(value)))
210            .ok_or_else(|| out_of_range("integer")),
211        ("int8", Value::Int(value)) => value
212            .checked_neg()
213            .map(Value::Int)
214            .ok_or_else(|| out_of_range("bigint")),
215        ("float4" | "float8", Value::Float(value)) => Ok(Value::Float(-value)),
216        ("numeric", Value::Decimal(value)) => uqa_core::DecimalValue::from_i64(0)
217            .checked_sub(value)
218            .map(Value::Decimal)
219            .ok_or_else(|| out_of_range("numeric")),
220        (
221            "interval",
222            Value::Temporal(TemporalValue::Interval {
223                months,
224                days,
225                micros,
226            }),
227        ) => Ok(Value::Temporal(TemporalValue::Interval {
228            months: months
229                .checked_neg()
230                .ok_or_else(|| out_of_range("interval"))?,
231            days: days.checked_neg().ok_or_else(|| out_of_range("interval"))?,
232            micros: micros
233                .checked_neg()
234                .ok_or_else(|| out_of_range("interval"))?,
235        })),
236        _ => Err(SQLError::TypeMismatch(format!(
237            "operator does not exist: - {source}"
238        ))),
239    }
240}
241
242fn canonical_cast_source(source_ty: Option<&str>, value: &Value) -> String {
243    let source = source_ty.unwrap_or(match value {
244        Value::Str(_) | Value::FixedChar(_) => "unknown",
245        Value::Int(_) => "integer",
246        Value::Bool(_) => "boolean",
247        Value::Float(_) => "double precision",
248        Value::Decimal(_) => "numeric",
249        Value::Bytes(_) => "bytea",
250        Value::Temporal(TemporalValue::Interval { .. }) => "interval",
251        Value::Temporal(_) => "timestamp",
252        Value::Json(_) => "json",
253        Value::JsonB(_) => "jsonb",
254        Value::Array(_) => "anyarray",
255        Value::List(_) => "anyarray",
256        Value::Row(_) | Value::Record(_) => "record",
257        Value::Map(_) => "jsonb",
258        Value::Null => "unknown",
259    });
260    let (source, _) = split_type_modifier(source);
261    let source = source
262        .trim()
263        .to_ascii_lowercase()
264        .split_whitespace()
265        .collect::<Vec<_>>()
266        .join(" ");
267    let source = source.strip_prefix("pg_catalog.").unwrap_or(&source);
268    match source {
269        "smallint" | "int2" => "int2".into(),
270        "integer" | "int" | "int4" | "serial" | "serial4" => "int4".into(),
271        "bigint" | "int8" | "bigserial" | "serial8" => "int8".into(),
272        "character varying" | "varchar" => "varchar".into(),
273        "character" | "char" | "bpchar" => "bpchar".into(),
274        "boolean" | "bool" => "bool".into(),
275        "double" | "double precision" | "float8" => "float8".into(),
276        "real" | "float4" => "float4".into(),
277        other => other.into(),
278    }
279}
280
281fn cast_oid(value: &Value, source_ty: Option<&str>) -> Result<Value> {
282    let source = canonical_cast_source(source_ty, value);
283    match (source.as_str(), value) {
284        (
285            "unknown" | "text" | "varchar" | "bpchar" | "name",
286            Value::Str(text) | Value::FixedChar(text),
287        ) => parse_uint32_input(text, "oid"),
288        ("int2", Value::Int(value)) => {
289            let value = i16::try_from(*value).map_err(|_| out_of_range("smallint"))?;
290            Ok(Value::Int(i64::from(i32::from(value) as u32)))
291        }
292        ("int4", Value::Int(value)) => {
293            let value = i32::try_from(*value).map_err(|_| out_of_range("integer"))?;
294            Ok(Value::Int(i64::from(value as u32)))
295        }
296        ("int8", Value::Int(value)) => u32::try_from(*value)
297            .map(|value| Value::Int(i64::from(value)))
298            .map_err(|_| SQLError::Routine {
299                sqlstate: "22003".into(),
300                message: "OID out of range".into(),
301            }),
302        (
303            "oid" | "regclass" | "regcollation" | "regconfig" | "regdictionary" | "regnamespace"
304            | "regoper" | "regoperator" | "regproc" | "regprocedure" | "regrole" | "regtype",
305            Value::Int(value),
306        ) => u32::try_from(*value)
307            .map(|value| Value::Int(i64::from(value)))
308            .map_err(|_| out_of_range("oid")),
309        _ => Err(undefined_cast(&source, "oid")),
310    }
311}
312
313fn cast_regclass(value: &Value, source_ty: Option<&str>) -> Result<Value> {
314    let source = canonical_cast_source(source_ty, value);
315    match (source.as_str(), value) {
316        (
317            "unknown" | "text" | "varchar" | "bpchar" | "name" | "regclass",
318            Value::Str(text) | Value::FixedChar(text),
319        ) => Ok(Value::Str(text.clone())),
320        (_, Value::Int(_)) => cast_oid(value, source_ty),
321        _ => Err(undefined_cast(&source, "regclass")),
322    }
323}
324
325fn cast_regnamespace(value: &Value, source_ty: Option<&str>) -> Result<Value> {
326    let source = canonical_cast_source(source_ty, value);
327    match (source.as_str(), value) {
328        (
329            "unknown" | "text" | "varchar" | "bpchar" | "name" | "regnamespace",
330            Value::Str(text) | Value::FixedChar(text),
331        ) => Ok(Value::Str(text.clone())),
332        (_, Value::Int(_)) => cast_oid(value, source_ty),
333        _ => Err(undefined_cast(&source, "regnamespace")),
334    }
335}
336
337fn cast_xid(value: &Value, source_ty: Option<&str>) -> Result<Value> {
338    let source = canonical_cast_source(source_ty, value);
339    match (source.as_str(), value) {
340        (
341            "unknown" | "text" | "varchar" | "bpchar" | "name",
342            Value::Str(text) | Value::FixedChar(text),
343        ) => parse_uint32_input(text, "xid"),
344        ("xid", Value::Int(value)) => u32::try_from(*value)
345            .map(|value| Value::Int(i64::from(value)))
346            .map_err(|_| out_of_range("xid")),
347        _ => Err(undefined_cast(&source, "xid")),
348    }
349}
350
351fn cast_bytea(value: &Value, source_ty: Option<&str>) -> Result<Value> {
352    let source = canonical_cast_source(source_ty, value);
353    match (source.as_str(), value) {
354        ("bytea", Value::Bytes(bytes)) => Ok(Value::Bytes(bytes.clone())),
355        ("int2" | "int4" | "int8", Value::Int(value)) => integer_to_bytea(*value, Some(&source)),
356        (
357            "unknown" | "text" | "varchar" | "bpchar" | "name",
358            Value::Str(text) | Value::FixedChar(text),
359        ) => parse_bytea_input(text),
360        _ => Err(undefined_cast(&source, "bytea")),
361    }
362}
363
364fn parse_bytea_input(text: &str) -> Result<Value> {
365    if let Some(hex) = text.strip_prefix("\\x") {
366        if !hex.len().is_multiple_of(2) {
367            return Err(invalid_bytea(
368                "invalid hexadecimal data: odd number of digits",
369            ));
370        }
371        let mut bytes = Vec::with_capacity(hex.len() / 2);
372        for pair in hex.as_bytes().chunks_exact(2) {
373            let hi = (pair[0] as char)
374                .to_digit(16)
375                .ok_or_else(|| invalid_bytea("invalid hexadecimal digit"))?;
376            let lo = (pair[1] as char)
377                .to_digit(16)
378                .ok_or_else(|| invalid_bytea("invalid hexadecimal digit"))?;
379            bytes.push((hi * 16 + lo) as u8);
380        }
381        return Ok(Value::Bytes(bytes));
382    }
383
384    let input = text.as_bytes();
385    let mut output = Vec::with_capacity(input.len());
386    let mut index = 0;
387    while index < input.len() {
388        if input[index] != b'\\' {
389            output.push(input[index]);
390            index += 1;
391            continue;
392        }
393        if input.get(index + 1) == Some(&b'\\') {
394            output.push(b'\\');
395            index += 2;
396            continue;
397        }
398        let Some(octal) = input.get(index + 1..index + 4) else {
399            return Err(invalid_bytea("invalid input syntax for type bytea"));
400        };
401        if !matches!(octal[0], b'0'..=b'3')
402            || !octal[1..].iter().all(|byte| matches!(byte, b'0'..=b'7'))
403        {
404            return Err(invalid_bytea("invalid input syntax for type bytea"));
405        }
406        output.push((octal[0] - b'0') * 64 + (octal[1] - b'0') * 8 + (octal[2] - b'0'));
407        index += 4;
408    }
409    Ok(Value::Bytes(output))
410}
411
412fn invalid_bytea(message: &str) -> SQLError {
413    SQLError::Routine {
414        sqlstate: "22023".into(),
415        message: message.into(),
416    }
417}
418
419fn parse_uint32_input(text: &str, target: &str) -> Result<Value> {
420    let trimmed = text.trim();
421    let digits = trimmed
422        .strip_prefix('+')
423        .or_else(|| trimmed.strip_prefix('-'))
424        .unwrap_or(trimmed);
425    if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
426        return Err(SQLError::Routine {
427            sqlstate: "22P02".into(),
428            message: format!("invalid input syntax for type {target}: \"{text}\""),
429        });
430    }
431    let parsed = trimmed.parse::<i128>().map_err(|_| SQLError::Routine {
432        sqlstate: "22003".into(),
433        message: format!("value \"{text}\" is out of range for type {target}"),
434    })?;
435    if !((i128::from(i32::MIN))..=i128::from(u32::MAX)).contains(&parsed) {
436        return Err(SQLError::Routine {
437            sqlstate: "22003".into(),
438            message: format!("value \"{text}\" is out of range for type {target}"),
439        });
440    }
441    let value = if parsed < 0 {
442        u32::from_ne_bytes((parsed as i32).to_ne_bytes())
443    } else {
444        parsed as u32
445    };
446    Ok(Value::Int(i64::from(value)))
447}
448
449fn undefined_cast(source: &str, target: &str) -> SQLError {
450    SQLError::Routine {
451        sqlstate: "42846".into(),
452        message: format!("cannot cast type {source} to {target}"),
453    }
454}
455
456fn integer_to_bytea(value: i64, source_ty: Option<&str>) -> Result<Value> {
457    let source = source_ty
458        .map(split_type_modifier)
459        .map(|(base, _)| base)
460        .unwrap_or("integer");
461    let bytes = match source {
462        "smallint" | "int2" | "pg_catalog.int2" => i16::try_from(value)
463            .map(i16::to_be_bytes)
464            .map(|bytes| bytes.to_vec())
465            .map_err(|_| out_of_range("smallint"))?,
466        "bigint" | "int8" | "bigserial" | "serial8" | "pg_catalog.int8" => {
467            value.to_be_bytes().to_vec()
468        }
469        "integer" | "int" | "int4" | "serial" | "serial4" | "pg_catalog.int4" => {
470            i32::try_from(value)
471                .map(i32::to_be_bytes)
472                .map(|bytes| bytes.to_vec())
473                .map_err(|_| out_of_range("integer"))?
474        }
475        other => {
476            return Err(SQLError::TypeMismatch(format!(
477                "cannot cast {other} to bytea"
478            )));
479        }
480    };
481    Ok(Value::Bytes(bytes))
482}
483
484fn cast_uuid(value: &Value) -> Result<Value> {
485    let text = match value {
486        Value::Str(text) | Value::FixedChar(text) => text,
487        other => {
488            return Err(SQLError::TypeMismatch(format!(
489                "cannot cast {other:?} to uuid"
490            )))
491        }
492    };
493    let digits = text
494        .strip_prefix('{')
495        .and_then(|text| text.strip_suffix('}'))
496        .unwrap_or(text);
497    if digits.starts_with('{') || digits.ends_with('}') {
498        return Err(invalid_uuid(text));
499    }
500    let mut normalized = String::with_capacity(32);
501    let mut group_digits = 0_usize;
502    for character in digits.chars() {
503        if character == '-' {
504            if group_digits == 0 || !group_digits.is_multiple_of(4) {
505                return Err(invalid_uuid(text));
506            }
507            group_digits = 0;
508            continue;
509        }
510        if !character.is_ascii_hexdigit() {
511            return Err(invalid_uuid(text));
512        }
513        normalized.push(character.to_ascii_lowercase());
514        group_digits += 1;
515    }
516    if normalized.len() != 32 || group_digits == 0 {
517        return Err(invalid_uuid(text));
518    }
519    Ok(Value::Str(format!(
520        "{}-{}-{}-{}-{}",
521        &normalized[0..8],
522        &normalized[8..12],
523        &normalized[12..16],
524        &normalized[16..20],
525        &normalized[20..32]
526    )))
527}
528
529fn invalid_uuid(text: &str) -> SQLError {
530    SQLError::Routine {
531        sqlstate: "22P02".into(),
532        message: format!("invalid input syntax for type uuid: \"{text}\""),
533    }
534}
535
536/// Split `varchar(10)` / `numeric(10,2)` into `("varchar", Some("10"))`.
537pub(super) fn split_type_modifier(ty: &str) -> (&str, Option<&str>) {
538    match (ty.find('('), ty.rfind(')')) {
539        (Some(open), Some(close)) if close > open => {
540            (ty[..open].trim_end(), Some(&ty[open + 1..close]))
541        }
542        _ => (ty, None),
543    }
544}
545
546/// CAST to the integer family with `PostgreSQL` conversion rules:
547/// float8 rounds half-to-even, numeric rounds half-away-from-zero,
548/// strings must be integral text, and the result must fit the target
549/// width.
550pub(super) fn cast_integer(v: &Value, target: &str) -> Result<Value> {
551    let n: i64 = match v {
552        Value::Int(n) => *n,
553        Value::Bool(b) => i64::from(*b),
554        Value::Float(f) => {
555            if !f.is_finite() {
556                return Err(out_of_range(target));
557            }
558            let rounded = f.round_ties_even();
559            // `i64::MAX as f64` rounds up to 2^63.  Comparing with `>` would
560            // therefore admit 2^63 and Rust's float-to-int cast would silently
561            // saturate it to `i64::MAX`.
562            if rounded < i64::MIN as f64 || rounded >= 9_223_372_036_854_775_808.0 {
563                return Err(out_of_range(target));
564            }
565            rounded as i64
566        }
567        Value::Decimal(d) => d
568            .round_dp(0)
569            .to_i64_trunc()
570            .ok_or_else(|| out_of_range(target))?,
571        Value::Str(s) | Value::FixedChar(s) => {
572            s.trim().parse::<i64>().map_err(|_| SQLError::Routine {
573                sqlstate: "22P02".into(),
574                message: format!("invalid input syntax for type {target}: \"{s}\""),
575            })?
576        }
577        Value::Bytes(bytes) => bytea_to_integer(bytes, target)?,
578        other => {
579            return Err(SQLError::TypeMismatch(format!(
580                "cannot cast {other:?} to {target}"
581            )));
582        }
583    };
584    let in_range = match target {
585        "smallint" => i16::try_from(n).is_ok(),
586        "integer" => i32::try_from(n).is_ok(),
587        _ => true,
588    };
589    if !in_range {
590        return Err(out_of_range(target));
591    }
592    Ok(Value::Int(n))
593}
594
595fn bytea_to_integer(bytes: &[u8], target: &str) -> Result<i64> {
596    let width = match target {
597        "smallint" => 2,
598        "integer" => 4,
599        _ => 8,
600    };
601    if bytes.len() > width {
602        return Err(out_of_range(target));
603    }
604    let mut extended = [0_u8; 8];
605    let offset = width - bytes.len();
606    extended[8 - width + offset..].copy_from_slice(bytes);
607    Ok(match width {
608        2 => i64::from(i16::from_be_bytes([extended[6], extended[7]])),
609        4 => i64::from(i32::from_be_bytes([
610            extended[4],
611            extended[5],
612            extended[6],
613            extended[7],
614        ])),
615        _ => i64::from_be_bytes(extended),
616    })
617}
618
619/// CAST to boolean: strings follow `PostgreSQL`'s `parse_bool`
620/// (prefixes of true/false/yes/no, on/off, 1/0); numbers are non-zero
621/// tests.
622pub(super) fn cast_boolean(v: &Value) -> Result<Value> {
623    match v {
624        Value::Bool(b) => Ok(Value::Bool(*b)),
625        Value::Int(n) => Ok(Value::Bool(*n != 0)),
626        Value::Float(f) => Ok(Value::Bool(*f != 0.0)),
627        Value::Decimal(d) => Ok(Value::Bool(!d.is_zero())),
628        Value::Str(s) | Value::FixedChar(s) => {
629            let text = s.trim().to_ascii_lowercase();
630            let matches_prefix = |word: &str| !text.is_empty() && word.starts_with(&text);
631            let value = if matches_prefix("true") || matches_prefix("yes") || text == "1" {
632                Some(true)
633            } else if matches_prefix("false") || matches_prefix("no") || text == "0" {
634                Some(false)
635            } else if "on" == text {
636                Some(true)
637            } else if matches_prefix("off") && text.len() >= 2 {
638                Some(false)
639            } else {
640                None
641            };
642            value.map(Value::Bool).ok_or_else(|| SQLError::Routine {
643                sqlstate: "22P02".into(),
644                message: format!("invalid input syntax for type boolean: \"{s}\""),
645            })
646        }
647        other => Err(SQLError::TypeMismatch(format!(
648            "cannot cast {other:?} to boolean"
649        ))),
650    }
651}
652
653/// Parse a `PostgreSQL` array literal (`{1,2,3}`, `{"a b",NULL}`,
654/// `{{1,2},{3,4}}`) into nested lists of string/NULL values; the caller
655/// casts elements.
656pub fn parse_pg_array_literal(text: &str) -> Result<ArrayValue> {
657    let mut parser = PgArrayLiteralParser::new(text);
658    let (declared_dimensions, items) = parser.parse()?;
659    if let Err(error) = array_shape(&items) {
660        return Err(SQLError::Routine {
661            sqlstate: "22P02".into(),
662            message: format!("malformed array literal: \"{text}\" ({})", error.message()),
663        });
664    }
665    let array = ArrayValue::try_new(items).ok_or_else(|| SQLError::Routine {
666        sqlstate: "22P02".into(),
667        message: format!("malformed array literal: \"{text}\""),
668    })?;
669    let Some(declared_dimensions) = declared_dimensions else {
670        return Ok(array);
671    };
672    let declared_lengths = declared_dimensions
673        .iter()
674        .map(|(_, length)| *length)
675        .collect::<Vec<_>>();
676    if declared_lengths != array.dimensions() {
677        return Err(SQLError::Routine {
678            sqlstate: "22P02".into(),
679            message: format!(
680                "malformed array literal: \"{text}\" (specified array dimensions do not match array contents)"
681            ),
682        });
683    }
684    let lower_bounds = declared_dimensions
685        .into_iter()
686        .map(|(lower, _)| lower)
687        .collect();
688    ArrayValue::with_lower_bounds(array.into_elements(), lower_bounds).ok_or_else(|| {
689        SQLError::Routine {
690            sqlstate: "22P02".into(),
691            message: format!("malformed array literal: \"{text}\""),
692        }
693    })
694}
695
696fn cast_array_elements(
697    items: &[Value],
698    element_type: &str,
699    source_element_type: Option<&str>,
700) -> Result<Vec<Value>> {
701    items
702        .iter()
703        .map(|item| match item {
704            Value::List(nested) => {
705                cast_array_elements(nested, element_type, source_element_type).map(Value::List)
706            }
707            other => cast_value_from(other, element_type, source_element_type),
708        })
709        .collect()
710}
711
712pub(super) struct PgArrayLiteralParser<'a> {
713    source: &'a str,
714    chars: std::iter::Peekable<std::str::Chars<'a>>,
715}
716
717type ParsedArrayLiteral = (Option<Vec<(i32, usize)>>, Vec<Value>);
718
719impl<'a> PgArrayLiteralParser<'a> {
720    fn new(source: &'a str) -> Self {
721        Self {
722            source,
723            chars: source.chars().peekable(),
724        }
725    }
726
727    fn parse(&mut self) -> Result<ParsedArrayLiteral> {
728        self.skip_whitespace();
729        let dimensions = self.parse_dimension_declaration()?;
730        let items = self.parse_array()?;
731        self.skip_whitespace();
732        if self.chars.peek().is_some() {
733            return Err(self.error("unexpected content after closing brace"));
734        }
735        Ok((dimensions, items))
736    }
737
738    fn parse_dimension_declaration(&mut self) -> Result<Option<Vec<(i32, usize)>>> {
739        if self.chars.peek() != Some(&'[') {
740            return Ok(None);
741        }
742        let mut dimensions = Vec::new();
743        while self.chars.next_if_eq(&'[').is_some() {
744            self.skip_whitespace();
745            let lower = self.parse_dimension_bound()?;
746            self.skip_whitespace();
747            if self.chars.next() != Some(':') {
748                return Err(self.error("array dimension must contain `:`"));
749            }
750            self.skip_whitespace();
751            let upper = self.parse_dimension_bound()?;
752            self.skip_whitespace();
753            if self.chars.next() != Some(']') {
754                return Err(self.error("array dimension is missing a closing `]`"));
755            }
756            if upper == i32::MAX {
757                return Err(SQLError::Routine {
758                    sqlstate: "54000".into(),
759                    message: format!("array upper bound is too large: {upper}"),
760                });
761            }
762            if upper < lower {
763                return Err(SQLError::Routine {
764                    sqlstate: "2202E".into(),
765                    message: "upper bound cannot be less than lower bound".into(),
766                });
767            }
768            let length = i64::from(upper)
769                .checked_sub(i64::from(lower))
770                .and_then(|difference| difference.checked_add(1))
771                .and_then(|length| usize::try_from(length).ok())
772                .ok_or_else(|| self.error("array dimension is out of range"))?;
773            dimensions.push((lower, length));
774            self.skip_whitespace();
775        }
776        if self.chars.next() != Some('=') {
777            return Err(self.error("array dimensions must be followed by `=`"));
778        }
779        self.skip_whitespace();
780        Ok(Some(dimensions))
781    }
782
783    fn parse_dimension_bound(&mut self) -> Result<i32> {
784        let mut text = String::new();
785        if self
786            .chars
787            .peek()
788            .is_some_and(|character| matches!(character, '+' | '-'))
789        {
790            text.push(self.chars.next().expect("peeked array bound sign"));
791        }
792        while self.chars.peek().is_some_and(char::is_ascii_digit) {
793            text.push(self.chars.next().expect("peeked array bound digit"));
794        }
795        if text.is_empty() || matches!(text.as_str(), "+" | "-") {
796            return Err(self.error("array dimension bound must be an integer"));
797        }
798        text.parse()
799            .map_err(|_| self.error("array dimension bound is out of range"))
800    }
801
802    fn parse_array(&mut self) -> Result<Vec<Value>> {
803        if self.chars.next() != Some('{') {
804            return Err(self.error("array value must start with `{`"));
805        }
806        self.skip_whitespace();
807        if self.chars.next_if_eq(&'}').is_some() {
808            return Ok(Vec::new());
809        }
810
811        let mut items = Vec::new();
812        loop {
813            self.skip_whitespace();
814            items.push(self.parse_element()?);
815            self.skip_whitespace();
816            match self.chars.next() {
817                Some(',') => {
818                    self.skip_whitespace();
819                    if matches!(self.chars.peek(), None | Some('}')) {
820                        return Err(self.error("array contains a missing element"));
821                    }
822                }
823                Some('}') => break,
824                Some(_) => {
825                    return Err(self.error("array elements must be separated by commas"));
826                }
827                None => return Err(self.error("array is missing a closing `}`")),
828            }
829        }
830        Ok(items)
831    }
832
833    fn parse_element(&mut self) -> Result<Value> {
834        match self.chars.peek() {
835            Some('{') => self.parse_array().map(Value::List),
836            Some('"') => self.parse_quoted_element().map(Value::Str),
837            Some(',') | Some('}') | None => Err(self.error("array contains a missing element")),
838            Some(_) => self.parse_unquoted_element(),
839        }
840    }
841
842    fn parse_quoted_element(&mut self) -> Result<String> {
843        let _opening_quote = self.chars.next();
844        let mut value = String::new();
845        loop {
846            match self.chars.next() {
847                Some('"') => return Ok(value),
848                Some('\\') => value.push(
849                    self.chars
850                        .next()
851                        .ok_or_else(|| self.error("quoted element ends with an escape"))?,
852                ),
853                Some(character) => value.push(character),
854                None => return Err(self.error("array contains an unterminated quoted element")),
855            }
856        }
857    }
858
859    fn parse_unquoted_element(&mut self) -> Result<Value> {
860        let mut value = String::new();
861        let mut significant_len = 0;
862        let mut was_escaped = false;
863        while let Some(character) = self.chars.peek().copied() {
864            match character {
865                ',' | '}' => break,
866                '{' | '"' => {
867                    return Err(self.error("array contains an unescaped special character"));
868                }
869                '\\' => {
870                    let _escape = self.chars.next();
871                    let escaped = self
872                        .chars
873                        .next()
874                        .ok_or_else(|| self.error("array element ends with an escape"))?;
875                    value.push(escaped);
876                    significant_len = value.len();
877                    was_escaped = true;
878                }
879                _ => {
880                    let _character = self.chars.next();
881                    value.push(character);
882                    if !character.is_whitespace() {
883                        significant_len = value.len();
884                    }
885                }
886            }
887        }
888        value.truncate(significant_len);
889        if value.is_empty() {
890            return Err(self.error("array contains a missing element"));
891        }
892        if !was_escaped && value.eq_ignore_ascii_case("null") {
893            Ok(Value::Null)
894        } else {
895            Ok(Value::Str(value))
896        }
897    }
898
899    fn skip_whitespace(&mut self) {
900        while self
901            .chars
902            .next_if(|character| character.is_whitespace())
903            .is_some()
904        {}
905    }
906
907    fn error(&self, detail: &str) -> SQLError {
908        SQLError::Routine {
909            sqlstate: "22P02".into(),
910            message: format!("malformed array literal: \"{}\" ({detail})", self.source),
911        }
912    }
913}
914
915#[derive(Clone, Copy, Debug, PartialEq, Eq)]
916pub(super) enum ArrayShapeError {
917    MixedNesting,
918    MismatchedDimensions,
919}
920
921impl ArrayShapeError {
922    fn message(self) -> &'static str {
923        match self {
924            Self::MixedNesting => "cannot mix nested arrays and scalar elements",
925            Self::MismatchedDimensions => "multidimensional arrays must have matching dimensions",
926        }
927    }
928}
929
930pub(super) fn array_shape(items: &[Value]) -> std::result::Result<Vec<usize>, ArrayShapeError> {
931    let mut dimensions = vec![items.len()];
932    let mut nested_shape: Option<Vec<usize>> = None;
933    let mut has_scalar = false;
934    for item in items {
935        if let Value::List(nested) = item {
936            let shape = array_shape(nested)?;
937            if has_scalar {
938                return Err(ArrayShapeError::MixedNesting);
939            }
940            if nested_shape
941                .as_ref()
942                .is_some_and(|expected| *expected != shape)
943            {
944                return Err(ArrayShapeError::MismatchedDimensions);
945            }
946            nested_shape = Some(shape);
947        } else {
948            if nested_shape.is_some() {
949                return Err(ArrayShapeError::MixedNesting);
950            }
951            has_scalar = true;
952        }
953    }
954    if let Some(shape) = nested_shape {
955        dimensions.extend(shape);
956    }
957    Ok(dimensions)
958}
959
960/// Return every dimension of a rectangular array value.
961///
962/// `PostgreSQL` arrays cannot mix scalar and nested elements or contain
963/// sub-arrays with different extents.
964pub fn array_dimensions(items: &[Value]) -> Result<Vec<usize>> {
965    array_shape(items).map_err(|error| SQLError::TypeMismatch(error.message().to_string()))
966}
967
968#[derive(Clone, Copy)]
969pub(super) enum TemporalCastTarget {
970    Date,
971    Time,
972    TimeTz,
973    Timestamp,
974    TimestampTz,
975    Interval,
976}
977
978pub(super) fn cast_temporal(
979    v: &Value,
980    target: TemporalCastTarget,
981    parse: fn(&str) -> Option<TemporalValue>,
982    ty: &str,
983) -> Result<Value> {
984    match v {
985        Value::Temporal(value) => cast_temporal_kind(value, target)
986            .map(Value::Temporal)
987            .ok_or_else(|| SQLError::TypeMismatch(format!("cannot cast {v:?} to {ty}"))),
988        other => parse(&value_to_string(other))
989            .map(Value::Temporal)
990            .ok_or_else(|| SQLError::TypeMismatch(format!("cannot cast {v:?} to {ty}"))),
991    }
992}
993
994fn cast_date(v: &Value, source_ty: Option<&str>) -> Result<Value> {
995    match v {
996        Value::Temporal(value) => cast_temporal_kind(value, TemporalCastTarget::Date)
997            .map(Value::Temporal)
998            .ok_or_else(|| undefined_cast(&canonical_cast_source(source_ty, v), "date")),
999        Value::Str(text) | Value::FixedChar(text) => TemporalValue::try_parse_date(text)
1000            .map(Value::Temporal)
1001            .map_err(|error| {
1002                let field_overflow = matches!(
1003                    error.kind(),
1004                    chrono::format::ParseErrorKind::OutOfRange
1005                        | chrono::format::ParseErrorKind::Impossible
1006                );
1007                SQLError::Routine {
1008                    sqlstate: if field_overflow { "22008" } else { "22007" }.into(),
1009                    message: if field_overflow {
1010                        format!("date/time field value out of range: \"{text}\"")
1011                    } else {
1012                        format!("invalid input syntax for type date: \"{text}\"")
1013                    },
1014                }
1015            }),
1016        _ => Err(undefined_cast(&canonical_cast_source(source_ty, v), "date")),
1017    }
1018}
1019
1020fn cast_temporal_kind(value: &TemporalValue, target: TemporalCastTarget) -> Option<TemporalValue> {
1021    const MICROS_PER_DAY: i64 = 86_400_000_000;
1022    match (target, value) {
1023        (TemporalCastTarget::Date, TemporalValue::Date { days }) => {
1024            Some(TemporalValue::Date { days: *days })
1025        }
1026        (
1027            TemporalCastTarget::Date,
1028            TemporalValue::Timestamp { micros } | TemporalValue::TimestampTz { micros },
1029        ) => Some(TemporalValue::Date {
1030            days: i32::try_from(micros.div_euclid(MICROS_PER_DAY)).ok()?,
1031        }),
1032        (TemporalCastTarget::Time, TemporalValue::Time { micros })
1033        | (TemporalCastTarget::Time, TemporalValue::TimeTz { micros, .. })
1034        | (
1035            TemporalCastTarget::Time,
1036            TemporalValue::Timestamp { micros } | TemporalValue::TimestampTz { micros },
1037        )
1038        | (TemporalCastTarget::Time, TemporalValue::Interval { micros, .. }) => {
1039            Some(TemporalValue::Time {
1040                micros: micros.rem_euclid(MICROS_PER_DAY),
1041            })
1042        }
1043        (
1044            TemporalCastTarget::TimeTz,
1045            TemporalValue::TimeTz {
1046                micros,
1047                offset_minutes,
1048            },
1049        ) => Some(TemporalValue::TimeTz {
1050            micros: *micros,
1051            offset_minutes: *offset_minutes,
1052        }),
1053        (TemporalCastTarget::TimeTz, TemporalValue::Time { micros })
1054        | (TemporalCastTarget::TimeTz, TemporalValue::TimestampTz { micros }) => {
1055            Some(TemporalValue::TimeTz {
1056                micros: micros.rem_euclid(MICROS_PER_DAY),
1057                offset_minutes: 0,
1058            })
1059        }
1060        (TemporalCastTarget::Timestamp, TemporalValue::Timestamp { micros })
1061        | (TemporalCastTarget::Timestamp, TemporalValue::TimestampTz { micros }) => {
1062            Some(TemporalValue::Timestamp { micros: *micros })
1063        }
1064        (TemporalCastTarget::Timestamp, TemporalValue::Date { days }) => {
1065            Some(TemporalValue::Timestamp {
1066                micros: i64::from(*days).checked_mul(MICROS_PER_DAY)?,
1067            })
1068        }
1069        (TemporalCastTarget::TimestampTz, TemporalValue::TimestampTz { micros })
1070        | (TemporalCastTarget::TimestampTz, TemporalValue::Timestamp { micros }) => {
1071            Some(TemporalValue::TimestampTz { micros: *micros })
1072        }
1073        (TemporalCastTarget::TimestampTz, TemporalValue::Date { days }) => {
1074            Some(TemporalValue::TimestampTz {
1075                micros: i64::from(*days).checked_mul(MICROS_PER_DAY)?,
1076            })
1077        }
1078        (
1079            TemporalCastTarget::Interval,
1080            TemporalValue::Interval {
1081                months,
1082                days,
1083                micros,
1084            },
1085        ) => Some(TemporalValue::Interval {
1086            months: *months,
1087            days: *days,
1088            micros: *micros,
1089        }),
1090        (TemporalCastTarget::Interval, TemporalValue::Time { micros }) => {
1091            Some(TemporalValue::Interval {
1092                months: 0,
1093                days: 0,
1094                micros: *micros,
1095            })
1096        }
1097        _ => None,
1098    }
1099}
1100
1101#[cfg(test)]
1102mod tests {
1103    use super::*;
1104
1105    #[test]
1106    fn array_literal_rejects_postgresql_unrepresentable_upper_bound() {
1107        let error = parse_pg_array_literal("[2147483647:2147483647]={1}").unwrap_err();
1108        assert_eq!(error.sqlstate(), Some("54000"));
1109        assert_eq!(
1110            error.to_string(),
1111            "array upper bound is too large: 2147483647"
1112        );
1113        assert!(parse_pg_array_literal("[2147483646:2147483646]={1}").is_ok());
1114    }
1115    use uqa_core::DecimalValue;
1116
1117    #[test]
1118    fn temporal_cross_casts_convert_the_carrier_kind() {
1119        let date = Value::Temporal(TemporalValue::parse_date("2020-01-02").unwrap());
1120        assert_eq!(
1121            cast_value(&date, "timestamp").unwrap(),
1122            Value::Temporal(TemporalValue::parse_timestamp("2020-01-02 00:00:00").unwrap())
1123        );
1124        let timestamp =
1125            Value::Temporal(TemporalValue::parse_timestamp("2020-01-02 03:04:05").unwrap());
1126        assert_eq!(
1127            cast_value(&timestamp, "date").unwrap(),
1128            Value::Temporal(TemporalValue::parse_date("2020-01-02").unwrap())
1129        );
1130        assert_eq!(
1131            cast_value(&timestamp, "time").unwrap(),
1132            Value::Temporal(TemporalValue::parse_time("03:04:05").unwrap())
1133        );
1134        let interval = Value::Temporal(TemporalValue::parse_interval("1 day 25:02:03").unwrap());
1135        assert_eq!(
1136            cast_value(&interval, "time").unwrap(),
1137            Value::Temporal(TemporalValue::parse_time("01:02:03").unwrap())
1138        );
1139    }
1140
1141    #[test]
1142    fn uuid_cast_matches_postgresql_input_and_canonical_output() {
1143        for input in [
1144            "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11",
1145            "a0eebc999c0b4ef8bb6d6bb9bd380a11",
1146            "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}",
1147            "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11",
1148        ] {
1149            assert_eq!(
1150                cast_value(&Value::Str(input.into()), "uuid").unwrap(),
1151                Value::Str("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11".into())
1152            );
1153        }
1154    }
1155
1156    #[test]
1157    fn uuid_cast_rejects_postgresql_invalid_forms() {
1158        for input in [
1159            " a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 ",
1160            "a0e-ebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
1161            "not-a-uuid",
1162        ] {
1163            let error = cast_value(&Value::Str(input.into()), "uuid").unwrap_err();
1164            assert_eq!(error.sqlstate(), Some("22P02"));
1165        }
1166    }
1167
1168    #[test]
1169    fn oid_cast_preserves_postgresql_source_type_rules() {
1170        assert_eq!(
1171            cast_value_from(&Value::Int(-1), "oid", Some("smallint")).unwrap(),
1172            Value::Int(i64::from(u32::MAX))
1173        );
1174        assert_eq!(
1175            cast_value_from(&Value::Int(-1), "oid", Some("integer")).unwrap(),
1176            Value::Int(i64::from(u32::MAX))
1177        );
1178        assert_eq!(
1179            cast_value_from(&Value::Int(i64::from(u32::MAX)), "oid", Some("bigint")).unwrap(),
1180            Value::Int(i64::from(u32::MAX))
1181        );
1182        let error = cast_value_from(&Value::Int(-1), "oid", Some("bigint")).unwrap_err();
1183        assert_eq!(error.sqlstate(), Some("22003"));
1184        assert_eq!(error.to_string(), "OID out of range");
1185        for source in ["boolean", "numeric", "double precision"] {
1186            let value = match source {
1187                "boolean" => Value::Bool(true),
1188                "numeric" => Value::Decimal(DecimalValue::from_i64(1)),
1189                _ => Value::Float(1.0),
1190            };
1191            let error = cast_value_from(&value, "oid", Some(source)).unwrap_err();
1192            assert_eq!(error.sqlstate(), Some("42846"));
1193        }
1194    }
1195
1196    #[test]
1197    fn regclass_cast_preserves_bound_relation_names_and_oid_carriers() {
1198        assert_eq!(
1199            cast_value_from(
1200                &Value::Str("app.items".into()),
1201                "pg_catalog.regclass",
1202                Some("unknown")
1203            )
1204            .unwrap(),
1205            Value::Str("app.items".into())
1206        );
1207        assert_eq!(
1208            cast_value_from(&Value::Int(2205), "regclass", Some("oid")).unwrap(),
1209            Value::Int(2205)
1210        );
1211    }
1212
1213    #[test]
1214    fn oid_and_xid_text_input_use_postgresql_uint32_syntax() {
1215        for target in ["oid", "xid"] {
1216            assert_eq!(
1217                cast_value(&Value::Str("-1".into()), target).unwrap(),
1218                Value::Int(i64::from(u32::MAX))
1219            );
1220            assert_eq!(
1221                cast_value(&Value::Str(i32::MIN.to_string()), target).unwrap(),
1222                Value::Int(i64::from(i32::MIN as u32))
1223            );
1224            assert_eq!(
1225                cast_value(&Value::Str(u32::MAX.to_string()), target).unwrap(),
1226                Value::Int(i64::from(u32::MAX))
1227            );
1228            for input in ["-2147483649", "4294967296"] {
1229                let error = cast_value(&Value::Str(input.into()), target).unwrap_err();
1230                assert_eq!(error.sqlstate(), Some("22003"));
1231            }
1232            let error = cast_value(&Value::Str("1.0".into()), target).unwrap_err();
1233            assert_eq!(error.sqlstate(), Some("22P02"));
1234        }
1235    }
1236
1237    #[test]
1238    fn xid_rejects_integer_and_oid_cast_sources() {
1239        for source in ["smallint", "integer", "bigint", "oid"] {
1240            let error = cast_value_from(&Value::Int(1), "xid", Some(source)).unwrap_err();
1241            assert_eq!(error.sqlstate(), Some("42846"));
1242        }
1243    }
1244
1245    #[test]
1246    fn bytea_cast_preserves_postgresql_source_type_and_input_rules() {
1247        assert_eq!(
1248            cast_value_from(&Value::Int(-1), "bytea", Some("smallint")).unwrap(),
1249            Value::Bytes(vec![0xff, 0xff])
1250        );
1251        assert_eq!(
1252            cast_value_from(&Value::Int(-1), "bytea", Some("integer")).unwrap(),
1253            Value::Bytes(vec![0xff; 4])
1254        );
1255        assert_eq!(
1256            cast_value_from(&Value::Int(-1), "bytea", Some("bigint")).unwrap(),
1257            Value::Bytes(vec![0xff; 8])
1258        );
1259        assert_eq!(
1260            cast_value_from(&Value::Str("\\x6162".into()), "bytea", Some("text")).unwrap(),
1261            Value::Bytes(b"ab".to_vec())
1262        );
1263        assert_eq!(
1264            cast_value_from(&Value::Str("a\\\\b\\141".into()), "bytea", Some("text")).unwrap(),
1265            Value::Bytes(b"a\\ba".to_vec())
1266        );
1267        for (value, source) in [
1268            (Value::Bool(true), "boolean"),
1269            (Value::Decimal(DecimalValue::from_i64(1)), "numeric"),
1270            (Value::Float(1.0), "double precision"),
1271        ] {
1272            let error = cast_value_from(&value, "bytea", Some(source)).unwrap_err();
1273            assert_eq!(error.sqlstate(), Some("42846"));
1274        }
1275        for input in ["\\x1", "\\xzz", "\\9"] {
1276            let error = cast_value(&Value::Str(input.into()), "bytea").unwrap_err();
1277            assert_eq!(error.sqlstate(), Some("22023"));
1278        }
1279    }
1280
1281    #[test]
1282    fn unary_minus_preserves_integer_width_and_overflow() {
1283        for (source, input, expected) in [
1284            ("smallint", 1_i64, -1_i64),
1285            ("integer", 1_i64, -1_i64),
1286            ("bigint", 1_i64, -1_i64),
1287        ] {
1288            assert_eq!(
1289                negate_value(&Value::Int(input), Some(source)).unwrap(),
1290                Value::Int(expected)
1291            );
1292        }
1293        for (source, minimum) in [
1294            ("smallint", i64::from(i16::MIN)),
1295            ("integer", i64::from(i32::MIN)),
1296            ("bigint", i64::MIN),
1297        ] {
1298            let error = negate_value(&Value::Int(minimum), Some(source)).unwrap_err();
1299            assert_eq!(error.sqlstate(), Some("22003"));
1300        }
1301    }
1302
1303    #[test]
1304    fn unary_minus_preserves_interval_fields() {
1305        assert_eq!(
1306            negate_value(
1307                &Value::Temporal(TemporalValue::Interval {
1308                    months: 2,
1309                    days: -3,
1310                    micros: 4,
1311                }),
1312                Some("interval"),
1313            )
1314            .unwrap(),
1315            Value::Temporal(TemporalValue::Interval {
1316                months: -2,
1317                days: 3,
1318                micros: -4,
1319            })
1320        );
1321    }
1322}