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