Skip to main content

spg_engine/eval/
cast.rs

1//! `expr::TYPE` CAST evaluation (cut 29 — extracted from `eval.rs`).
2//!
3//! Implements PG-style runtime coercion: the giant `cast_value`
4//! dispatcher plus its per-target helpers (numeric / bool / array /
5//! date / timestamp / interval / vector). Date and timestamp casts
6//! defer to the calendar parsers (`parse_date_literal` /
7//! `parse_timestamp_literal`) that stay in `eval.rs`; tsvector /
8//! tsquery casts defer to the FTS codecs re-exported from
9//! `eval::textsearch`.
10
11use alloc::format;
12use alloc::string::{String, ToString};
13use alloc::vec::Vec;
14
15use spg_sql::ast::CastTarget;
16use spg_storage::Value;
17
18use super::math::{f64_powi, f64_round_half_even};
19use super::{
20    EvalError, decode_tsquery_external, decode_tsvector_external, parse_date_literal,
21    parse_timestamp_literal, value_to_text,
22};
23
24/// v7.39 (read01 regproc.c) — the reg* input types SPG carries as text:
25/// resolve the name and return its canonical rendering, with PG's
26/// distinct not-found / ambiguous errors.
27/// v7.39 (round 515) — the type names each family below accepts, declared
28/// ONCE so the value path and the NULL-target check cannot drift apart.
29///
30/// They already had, three times. Round 509 added a check that a cast target
31/// names something, with its own hand-kept list; round 512 then added
32/// `Value::Cid` without registering `cid` there, round 514 found that and
33/// `xid`, and round 515 found six more — `'english'::regconfig` resolved
34/// while `NULL::regconfig` did not. The duplication was the defect, so it
35/// is gone rather than patched a third time.
36pub(crate) const REG_MISC_TYPES: &[&str] = &[
37    "regproc",
38    "regprocedure",
39    "regoper",
40    "regoperator",
41    "regconfig",
42    "regdictionary",
43    "regcollation",
44];
45
46/// The catalog-shaped scalars — see `cast_catalog_scalar`.
47pub(crate) const CATALOG_SCALAR_TYPES: &[&str] = &[
48    "cid",
49    "xid",
50    "oidvector",
51    "int2vector",
52    "aclitem",
53    "refcursor",
54    "pg_snapshot",
55    "txid_snapshot",
56    "jsonpath",
57];
58
59/// The pseudotypes and the statistics / GiST internals: a NULL is NULL, a
60/// value is "cannot accept a value of type X".
61pub(crate) const OPAQUE_TYPES: &[&str] = &[
62    "anyarray",
63    "anyelement",
64    "anyenum",
65    "anyrange",
66    "anymultirange",
67    "anynonarray",
68    "anycompatible",
69    "anycompatiblearray",
70    "anycompatiblenonarray",
71    "anycompatiblerange",
72    "anycompatiblemultirange",
73    "any",
74    "trigger",
75    "event_trigger",
76    "internal",
77    "language_handler",
78    "fdw_handler",
79    "pg_ddl_command",
80    "pg_node_tree",
81    "pg_ndistinct",
82    "pg_mcv_list",
83    "pg_dependencies",
84    "pg_brin_minmax_multi_summary",
85    "pg_brin_bloom_summary",
86    "gtsvector",
87];
88
89fn cast_reg_misc(kind: &str, s: &str) -> Result<Value<'static>, EvalError> {
90    let bare = s
91        .strip_prefix("pg_catalog.")
92        .unwrap_or(s)
93        .trim()
94        .to_string();
95    match kind {
96        "regconfig" => {
97            const CONFIGS: &[&str] = &[
98                "simple",
99                "arabic",
100                "armenian",
101                "basque",
102                "catalan",
103                "danish",
104                "dutch",
105                "english",
106                "finnish",
107                "french",
108                "german",
109                "greek",
110                "hindi",
111                "hungarian",
112                "indonesian",
113                "irish",
114                "italian",
115                "lithuanian",
116                "nepali",
117                "norwegian",
118                "portuguese",
119                "romanian",
120                "russian",
121                "serbian",
122                "spanish",
123                "swedish",
124                "tamil",
125                "turkish",
126                "yiddish",
127            ];
128            if CONFIGS.contains(&bare.as_str()) {
129                Ok(Value::text(bare))
130            } else {
131                Err(EvalError::TypeMismatch {
132                    detail: alloc::format!("text search configuration \"{s}\" does not exist"),
133                })
134            }
135        }
136        // v7.39 (round 513) — `regcollation`. PG lowercases an UNQUOTED
137        // identifier before it looks, which is why `'C'::regcollation` is
138        // "collation \"c\" for encoding \"UTF8\" does not exist" there while
139        // `'\"C\"'::regcollation` resolves — measured, and the reason the
140        // quoted form is the one anybody writes. The rendering keeps the
141        // quotes PG puts back on a name that needs them.
142        "regcollation" => {
143            let quoted = bare.starts_with('"') && bare.ends_with('"') && bare.len() >= 2;
144            let name = if quoted {
145                bare[1..bare.len() - 1].to_string()
146            } else {
147                bare.to_ascii_lowercase()
148            };
149            const COLLATIONS: &[&str] = &["C", "POSIX", "default", "ucs_basic"];
150            match COLLATIONS.iter().find(|c| **c == name) {
151                // PG re-quotes anything that is not a plain lowercase word.
152                Some(c) => Ok(Value::text(
153                    if c.chars().all(|ch| ch.is_ascii_lowercase() || ch == '_') && *c != "default" {
154                        (*c).to_string()
155                    } else {
156                        alloc::format!("\"{c}\"")
157                    },
158                )),
159                None => Err(EvalError::TypeMismatch {
160                    detail: alloc::format!(
161                        "collation \"{name}\" for encoding \"UTF8\" does not exist"
162                    ),
163                }),
164            }
165        }
166        "regdictionary" => {
167            if bare == "simple" || bare.ends_with("_stem") {
168                Ok(Value::text(bare))
169            } else {
170                Err(EvalError::TypeMismatch {
171                    detail: alloc::format!("text search dictionary \"{s}\" does not exist"),
172                })
173            }
174        }
175        "regproc" => {
176            let hits = crate::system_catalog::PG_PROC_FUNCS
177                .iter()
178                .filter(|(_, n, ..)| *n == bare)
179                .count();
180            match hits {
181                0 => Err(EvalError::TypeMismatch {
182                    detail: alloc::format!("function \"{s}\" does not exist"),
183                }),
184                1 => Ok(Value::text(bare)),
185                _ => Err(EvalError::TypeMismatch {
186                    detail: alloc::format!("more than one function named \"{bare}\""),
187                }),
188            }
189        }
190        "regprocedure" => {
191            // `name(argtype, ...)` — resolve the name, canonicalize each
192            // argument type, and re-render.
193            let Some((fname, rest)) = bare.split_once('(') else {
194                return Err(EvalError::TypeMismatch {
195                    detail: alloc::format!("expected a left parenthesis in \"{s}\""),
196                });
197            };
198            let Some(args_txt) = rest.strip_suffix(')') else {
199                return Err(EvalError::TypeMismatch {
200                    detail: alloc::format!("expected a right parenthesis in \"{s}\""),
201                });
202            };
203            let fname = fname.trim().to_ascii_lowercase();
204            let args: Vec<String> = if args_txt.trim().is_empty() {
205                Vec::new()
206            } else {
207                args_txt
208                    .split(',')
209                    .map(|a| {
210                        crate::conversions::regtype_canonical_name(a.trim()).ok_or_else(|| {
211                            EvalError::TypeMismatch {
212                                detail: alloc::format!("type \"{}\" does not exist", a.trim()),
213                            }
214                        })
215                    })
216                    .collect::<Result<_, _>>()?
217            };
218            let nargs = args.len() as i32;
219            let known = crate::system_catalog::PG_PROC_FUNCS
220                .iter()
221                .any(|(_, n, _, na, _)| *n == fname && *na == nargs);
222            if !known {
223                return Err(EvalError::TypeMismatch {
224                    detail: alloc::format!("function \"{s}\" does not exist"),
225                });
226            }
227            Ok(Value::text(alloc::format!("{fname}({})", args.join(","))))
228        }
229        // regoper / regoperator: SPG has no operator catalog; every core
230        // operator symbol is multiply overloaded in PG, so a known symbol
231        // reports PG's ambiguity and anything else does not exist.
232        _ => {
233            let sym: String = bare.chars().filter(|c| !c.is_whitespace()).collect();
234            let core_op = !sym.is_empty() && sym.chars().all(|c| "+-*/<>=~!@#%^&|`?".contains(c));
235            if core_op {
236                Err(EvalError::TypeMismatch {
237                    detail: alloc::format!("more than one operator named {sym}"),
238                })
239            } else {
240                Err(EvalError::TypeMismatch {
241                    detail: alloc::format!("operator does not exist: {s}"),
242                })
243            }
244        }
245    }
246}
247
248/// v7.39 (round 355, M13) — `BINARY expr` / `CAST(expr AS BINARY[(n)])`.
249fn cast_mysql_binary(v: Value<'static>, name: &str) -> Result<Value<'static>, EvalError> {
250    let limit: Option<usize> = name
251        .split_once('(')
252        .and_then(|(_, rest)| rest.trim_end_matches(')').trim().parse().ok());
253    // v7.39.2 — a binary source STAYS binary. It used to be rendered
254    // with PG's bytea text, so `CAST(0x41 AS BINARY)` answered the six
255    // characters `\x41` where MySQL 9.7.2 answers the byte 'A', and
256    // `HEX(CAST(0x00FF AS BINARY))` would have described that text
257    // rather than the bytes. `(n)` truncates to n BYTES there —
258    // `HEX(CAST(0x4142 AS BINARY(1)))` is 41, measured.
259    if let Value::Bytes(b) = &v {
260        let bytes = match limit {
261            Some(n) if b.len() > n => b[..n].to_vec(),
262            _ => b.clone().into_owned(),
263        };
264        return Ok(Value::bytes(bytes));
265    }
266    let text = match &v {
267        Value::Null => return Ok(Value::Null),
268        Value::Text(t) => t.to_string(),
269        Value::BpChar(t) => t.to_string(),
270        other => crate::eval::values::value_to_text(other),
271    };
272    Ok(Value::text(match limit {
273        // Byte-wise, which is the point of the type.
274        Some(n) if text.len() > n => {
275            let mut cut = n;
276            while cut > 0 && !text.is_char_boundary(cut) {
277                cut -= 1;
278            }
279            text[..cut].to_string()
280        }
281        _ => text,
282    }))
283}
284
285/// v7.39 (round 352, M8) — `CAST(x AS SIGNED)` / `CAST(x AS UNSIGNED)`.
286fn cast_mysql_integer(v: Value<'static>, unsigned: bool) -> Result<Value<'static>, EvalError> {
287    // v7.39 (round 527) — an EXACT integer source must not round-trip
288    // through f64. It loses precision above 2^53, and the float→int cast
289    // SATURATES, so `CAST(18446744073709551615 AS UNSIGNED)` answered
290    // 9223372036854775807 — a different number, with nothing to say so.
291    // The value stores, compares and sums correctly at full width
292    // (measured against MariaDB 11); only the cast reduced it.
293    let exact: Option<i128> = match &v {
294        Value::Bool(b) => Some(i128::from(u8::from(*b))),
295        Value::SmallInt(x) => Some(i128::from(*x)),
296        Value::Int(x) => Some(i128::from(*x)),
297        Value::BigInt(x) => Some(i128::from(*x)),
298        Value::Numeric {
299            scaled,
300            scale: 0,
301            kind: spg_storage::NumericKind::Finite,
302        } => Some(*scaled),
303        _ => None,
304    };
305    let rounded: i128 = match exact {
306        Some(n) => n,
307        None => {
308            let n: f64 = match &v {
309                Value::Null => return Ok(Value::Null),
310                Value::Float(x) => *x,
311                Value::Real(x) => f64::from(*x),
312                #[allow(clippy::cast_precision_loss)]
313                Value::Numeric { scaled, scale, .. } => {
314                    *scaled as f64 / 10_f64.powi(i32::from(*scale))
315                }
316                Value::Text(t) | Value::BpChar(t) => crate::eval::mysql_leading_number(t),
317                other => {
318                    return Err(EvalError::TypeMismatch {
319                        detail: alloc::format!(
320                            "cannot cast {} to integer",
321                            crate::conversions::pg_type_name_for_error_opt(other.data_type())
322                        ),
323                    });
324                }
325            };
326            // Half away from zero, which is what MariaDB does
327            // (2.5 → 3, -2.5 → -3).
328            let r = if n >= 0.0 {
329                (n + 0.5).floor()
330            } else {
331                (n - 0.5).ceil()
332            };
333            #[allow(clippy::cast_possible_truncation)]
334            let as_i64 = r as i64;
335            i128::from(as_i64)
336        }
337    };
338    if unsigned {
339        // MariaDB wraps a negative through the full u64 range.
340        let wrapped: u64 = if rounded < 0 {
341            #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
342            {
343                rounded as i64 as u64
344            }
345        } else {
346            u64::try_from(rounded).unwrap_or(u64::MAX)
347        };
348        // Above i64::MAX the value only fits the numeric carrier, which
349        // is the same one a BIGINT UNSIGNED column already uses.
350        return Ok(if wrapped > i64::MAX as u64 {
351            Value::Numeric {
352                scaled: i128::from(wrapped),
353                scale: 0,
354                kind: spg_storage::NumericKind::Finite,
355            }
356        } else {
357            #[allow(clippy::cast_possible_wrap)]
358            Value::BigInt(wrapped as i64)
359        });
360    }
361    Ok(Value::BigInt(
362        i64::try_from(rounded).unwrap_or(if rounded < 0 { i64::MIN } else { i64::MAX }),
363    ))
364}
365
366/// v7.39 (round 544) — the integer a bytea's bytes spell: big-endian,
367/// right-aligned into `width` bytes, sign-extended from the leading
368/// byte when the value already fills the width. Measured on PG18:
369/// `'\x05'::bytea::int8` is 5, `'\x'::bytea::int4` is 0,
370/// `'\xffffffff'::bytea::int4` is -1.
371#[inline(never)]
372fn bytea_to_integer(v: &Value<'static>, width: usize) -> Result<Value<'static>, EvalError> {
373    let Value::Bytes(b) = v else {
374        return Err(EvalError::TypeMismatch {
375            detail: alloc::string::String::from("expected bytea"),
376        });
377    };
378    if b.len() > width {
379        return Err(EvalError::TypeMismatch {
380            detail: alloc::format!("bytea of {} bytes is too wide for the target", b.len()),
381        });
382    }
383    let negative = b.len() == width && b.first().is_some_and(|f| *f & 0x80 != 0);
384    let mut acc: i64 = if negative { -1 } else { 0 };
385    for byte in b.iter() {
386        acc = (acc << 8) | i64::from(*byte);
387    }
388    Ok(if width == 4 {
389        Value::Int(i32::try_from(acc).unwrap_or(0))
390    } else {
391        Value::BigInt(acc)
392    })
393}
394
395/// Round a numeric operand (`scaled` × 10^-`scale`) to the nearest
396/// integer, half-away-from-zero — PG's `numeric → int` coercion rule.
397fn numeric_round_to_i128(scaled: i128, scale: u16) -> i128 {
398    let factor = 10_i128.pow(u32::from(scale));
399    let neg = scaled < 0;
400    let abs = scaled.unsigned_abs() as i128;
401    let q = abs / factor;
402    let r = abs % factor;
403    let mag = if 2 * r >= factor { q + 1 } else { q };
404    if neg { -mag } else { mag }
405}
406
407/// PG-style `expr::TYPE` coercion. NULL always casts as NULL.
408pub fn cast_value(v: Value<'static>, target: CastTarget) -> Result<Value<'static>, EvalError> {
409    cast_value_in(v, target, false)
410}
411
412/// v7.39 (round 352, M8) — `cast_value` with the session dialect, for the
413/// targets the two disagree about (`SIGNED` / `UNSIGNED` exist only in
414/// MySQL: PG says `type "signed" does not exist`, measured).
415pub fn cast_value_in(
416    v: Value<'static>,
417    target: CastTarget,
418    mysql: bool,
419) -> Result<Value<'static>, EvalError> {
420    cast_value_ref_in(v, &target, mysql)
421}
422
423/// v7.39 (round 607) — the same dispatch, taking the target by REFERENCE.
424///
425/// `eval_cast_arm` cloned the target for every row. For the settled variants
426/// that clone is free, which is why `id::FLOAT` allocated nothing a row while
427/// `id::REAL` — the same conversion under a name the parser leaves as
428/// `Named(String)` — allocated one just to hand the name over, and seven more
429/// re-deriving its lowercase form inside.
430/// v7.39.2 — is this cast target a character type, for the purpose of
431/// reading a binary string's bytes as its characters?
432///
433/// `BINARY` and `BINARY(n)` are deliberately absent: MySQL keeps those
434/// binary, and the bytes go out as bytes.
435fn cast_reads_bytes_as_characters(target: &CastTarget) -> bool {
436    match target {
437        CastTarget::Text => true,
438        CastTarget::Named(n) => {
439            let lc = n.to_ascii_lowercase();
440            let base = lc.split('(').next().unwrap_or(&lc);
441            matches!(
442                base,
443                "char" | "varchar" | "character" | "nchar" | "nvarchar" | "text"
444            )
445        }
446        _ => false,
447    }
448}
449
450pub fn cast_value_ref_in(
451    v: Value<'static>,
452    target: &CastTarget,
453    mysql: bool,
454) -> Result<Value<'static>, EvalError> {
455    // v7.37 (round 896) — the quoted spelling of these two arrives as
456    // `Named`, the bare one as its own variant, and only the variant had an
457    // arm. `::regclass` worked and `::"regclass"` answered `type "regclass"
458    // does not exist` — and quoted identifiers are what an ORM or pg_dump
459    // writes. Folding here rather than adding a second arm keeps one
460    // implementation: whatever the variant does, the quoted form now does.
461    // Round 894 fixed `tsvector` / `tsquery` the same way round and left
462    // these open because this path had not been read; it has now.
463    // v7.39.2 — MySQL reads a binary string's BYTES when it casts to a
464    // character type. This path rendered it PG's way instead, so
465    // `CAST(0x41 AS CHAR)` answered the six characters `\x41`, and the
466    // answers built on it were wrong without saying so: measured on MySQL
467    // 9.7.2, `CAST(0x41 AS CHAR)='A'` is 1 and was 0 here, and
468    // `LENGTH(CAST(0x4142 AS CHAR))` is 2 and was 6.
469    //
470    // `CAST(x AS CHAR)` lowers onto `Text` in the MySQL dialect (it is
471    // UNBOUNDED there, round 352); `CHAR(n)` / `VARCHAR(n)` arrive named.
472    let v = match &v {
473        Value::Bytes(b) if mysql && cast_reads_bytes_as_characters(target) => Value::text(
474            b.iter()
475                .map(|&x| x as char)
476                .collect::<alloc::string::String>(),
477        ),
478        _ => v,
479    };
480    if let CastTarget::Named(n) = target {
481        if n.eq_ignore_ascii_case("regclass") {
482            return cast_value_ref_in(v, &CastTarget::RegClass, mysql);
483        }
484        if n.eq_ignore_ascii_case("regtype") {
485            return cast_value_ref_in(v, &CastTarget::RegType, mysql);
486        }
487    }
488    // v7.39 (round 509) — PG validates the cast TARGET whatever the operand
489    // is: `NULL::nosuchtype` is an error there, not NULL. This returned early
490    // before ever looking at the target, so a misspelt type name silently
491    // produced NULL and `pg_typeof(NULL::nosuchtype)` answered `unknown`. A
492    // value operand DID error, so the gap was exactly the NULL case, in both
493    // spellings (`::t` and `CAST(… AS t)`).
494    //
495    // Only `Named` can fail to resolve; every other CastTarget is a variant
496    // the parser already settled. So a NULL keeps its short-circuit
497    // everywhere else and a Named target runs the real path, which is the
498    // only thing that knows every name that resolves. Writing a second
499    // resolver to check the name against looked simpler and was wrong: it
500    // missed `::binary` (the MySQL prefix's desugar), a table's row type,
501    // and the pseudotypes, all of which resolve further down this arm.
502    if matches!(v, Value::Null) {
503        return Ok(Value::Null);
504    }
505    match target {
506        CastTarget::Vector => cast_to_vector(v),
507        // v7.38 (read01) — the inet/cidr ::text cast shows the mask even for
508        // /32 and /128 (PG's cast-path form, unlike the display default).
509        CastTarget::Text => Ok(Value::text(match &v {
510            Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
511                crate::conversions::format_inet_full(*family, *bits, addr)
512            }
513            // v7.38 (read01, T11) — bpchar → text strips the trailing blanks
514            // (unlike the padded wire display).
515            Value::BpChar(s) => s.trim_end_matches(' ').to_string(),
516            // v7.39 (read01 ruleutils.c) — regclass::text is the name.
517            Value::RegClass(_, name) | Value::RegProc(_, name) | Value::RegType(_, name) => {
518                name.to_string()
519            }
520            _ => value_to_text(&v),
521        })),
522        // v7.39 (round 254) — the integer targets refuse a NUMERIC special
523        // outright (PG: `cannot convert NaN to integer`); without this the
524        // arms below read the special's canonical mantissa and answered 0.
525        // The float / numeric targets pass it through instead — handled in
526        // their own arms, which now consult `kind`.
527        // v7.39 (round 343) — an OID-typed reference casts to an integer
528        // the way PG's do (`'t'::regclass::bigint` is 27830 there). SPG
529        // reported `cannot cast None to bigint`: the integer path read the
530        // value's storage DataType, which these two deliberately do not
531        // have, and the message leaked that `None` to the client.
532        CastTarget::BigInt | CastTarget::Int
533            if matches!(
534                v,
535                Value::RegClass(..) | Value::RegProc(..) | Value::RegType(..)
536            ) =>
537        {
538            let (Value::RegClass(oid, _) | Value::RegProc(oid, _) | Value::RegType(oid, _)) = v
539            else {
540                unreachable!("guarded above")
541            };
542            Ok(if matches!(target, CastTarget::BigInt) {
543                Value::BigInt(oid)
544            } else {
545                Value::Int(i32::try_from(oid).unwrap_or(i32::MAX))
546            })
547        }
548        // v7.39 (round 544) — bytea reads back as the integer its bytes
549        // spell, big-endian and right-aligned. Measured on PG18:
550        // '\x05'::bytea::int8 is 5, '\x'::bytea::int4 is 0.
551        CastTarget::Int if matches!(v, Value::Bytes(_)) => bytea_to_integer(&v, 4),
552        CastTarget::BigInt if matches!(v, Value::Bytes(_)) => bytea_to_integer(&v, 8),
553        CastTarget::Int => {
554            cast_numeric_special_reject(&v, "integer").unwrap_or_else(|| cast_numeric_to_int(v))
555        }
556        CastTarget::BigInt => {
557            cast_numeric_special_reject(&v, "bigint").unwrap_or_else(|| cast_numeric_to_bigint(v))
558        }
559        CastTarget::Float => cast_numeric_to_float(v),
560        CastTarget::Bool => cast_to_bool(v),
561        CastTarget::Date => cast_to_date(v),
562        // TIMESTAMP and TIMESTAMPTZ share a runtime representation
563        // (i64 microseconds UTC) but NOT an input rule, and conflating
564        // the two silently stored the wrong instant. `::timestamp`
565        // keeps the wall clock a literal's offset was written against
566        // (round 289); `::timestamptz` CONVERTS by it. The evaluator's
567        // context-aware arm intercepts timestamptz before this point,
568        // so the difference only showed on the paths that reach here
569        // directly — INSERT VALUES folds its literals through
570        // `literal_expr_to_value_in`, and stored 10:00 for
571        // `'2020-01-01 10:00:00+02'::timestamptz` where PG stores
572        // 08:00 (round 310).
573        // v7.39 (round 423) — `CAST(x AS DATETIME)` / `AS TIMESTAMP` reach
574        // here as the dedicated variant rather than `Named`, so the MySQL
575        // "bare temporal type has fractional precision 0" rule has to be
576        // applied here too: MariaDB drops the fraction, PG keeps every
577        // microsecond.
578        CastTarget::Timestamp => {
579            let out = cast_to_timestamp(v)?;
580            Ok(if mysql {
581                round_temporal_to_precision(out, 0, true)
582            } else {
583                out
584            })
585        }
586        CastTarget::Timestamptz => cast_to_timestamptz(v),
587        // v7.9.25 — `expr::INTERVAL`. Currently only TEXT → Interval
588        // is supported (the mailrs idiom: `$1::INTERVAL` where the
589        // bound param is a string like `'7 days'`).
590        // v7.39 (round 544) — a time-of-day IS an interval of that
591        // length. Measured: '10:20:30.123456'::time::interval reads
592        // 10:20:30.123456 on PG18, fractional seconds and all.
593        CastTarget::Interval => match v {
594            Value::Time(us) => Ok(Value::Interval {
595                months: 0,
596                days: 0,
597                micros: us,
598                kind: spg_storage::IntervalKind::Finite,
599            }),
600            other => cast_to_interval(other),
601        },
602        // v7.9.25 — `::json` keeps the input text verbatim (PG's json
603        // type preserves whitespace / key order / duplicates).
604        CastTarget::Json => match v {
605            Value::Json(s) => Ok(Value::json(s)),
606            Value::Text(s) => Ok(Value::json(s)),
607            other => Err(EvalError::TypeMismatch {
608                detail: alloc::format!(
609                    "::json only accepts TEXT-shape inputs, got {}",
610                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
611                ),
612            }),
613        },
614        // v7.38 (read01) — `::jsonb` canonicalises like PG: object keys
615        // sorted (length, then bytes) + duplicates collapsed last-wins,
616        // `, ` / `: ` whitespace, and numbers normalised. Invalid JSON
617        // falls back to the verbatim text (validation stays a separate
618        // concern from this representation fix).
619        CastTarget::Jsonb => match v {
620            // v7.39 (read01 jsonb) — the explicit ::jsonb cast validates:
621            // invalid tokens (NaN / Infinity / malformed) error like PG
622            // instead of passing the raw text through.
623            Value::Json(s) | Value::Text(s) => match crate::json::canonicalize_jsonb(s.as_ref()) {
624                Ok(c) => Ok(Value::json(c)),
625                Err(_) => Err(EvalError::TypeMismatch {
626                    detail: alloc::string::String::from("invalid input syntax for type json"),
627                }),
628            },
629            other => Err(EvalError::TypeMismatch {
630                detail: alloc::format!(
631                    "::jsonb only accepts TEXT-shape inputs, got {}",
632                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
633                ),
634            }),
635        },
636        // v7.17.0 Phase 5.3 — `::regtype` / `::regclass`. PG
637        // semantics: each is a textual catalog-name surfacing as
638        // a numeric OID at the wire layer that renders back as
639        // the original name. SPG has no OID space, but pg_dump /
640        // mailrs / Django code uses the cast purely for textual
641        // round-trip — feeding `'public.t'::regclass::text` into
642        // a downstream `format(…)` or string concat. We map to
643        // that textual contract: Text in → Text out (the schema-
644        // qualifier `public.` is stripped to match PG's default
645        // search_path-aware rendering); numeric in → re-cast to
646        // Text as best-effort; anything else errors.
647        //
648        // Pre-3.3 / pre-5.3 (v7.9.26) the cast surfaced a clean
649        // error; this lifts to accept-and-textify so the dominant
650        // dump-loader pattern unblocks. SPG-shaped queries that
651        // genuinely need an OID for runtime joins are still
652        // documented as unsupported.
653        // v7.39 (round 694) — `'{text,int4}'::regtype[]`. PG canonicalises
654        // every ELEMENT (`int4` → `integer`) and rejects an unknown one, so
655        // the array runs the scalar's own name resolution per member rather
656        // than keeping the literal. `regclass[]` keeps its names — a
657        // relation name is already what PG prints.
658        CastTarget::Named(n) if n.eq_ignore_ascii_case("regtype_array") => {
659            let Value::Text(s) = &v else {
660                return Ok(v);
661            };
662            let body = s.trim();
663            let inner = body
664                .strip_prefix('{')
665                .and_then(|b| b.strip_suffix('}'))
666                .unwrap_or(body);
667            let mut out: Vec<Option<alloc::string::String>> = Vec::new();
668            for part in inner.split(',') {
669                let t = part.trim();
670                if t.is_empty() {
671                    continue;
672                }
673                if t.eq_ignore_ascii_case("NULL") {
674                    out.push(None);
675                    continue;
676                }
677                let bare = t.rsplit('.').next().unwrap_or(t);
678                match crate::conversions::regtype_canonical_name(bare) {
679                    Some(c) => out.push(Some(c)),
680                    None => {
681                        return Err(EvalError::TypeMismatch {
682                            detail: alloc::format!("type \"{t}\" does not exist"),
683                        });
684                    }
685                }
686            }
687            Ok(Value::TextArray(out))
688        }
689        CastTarget::RegType | CastTarget::RegClass => match v {
690            Value::Text(s) => {
691                // Strip an optional `<schema>.` prefix — PG's
692                // regclass render drops it when the schema is on
693                // the search_path; SPG is single-schema so
694                // dropping is always safe.
695                let bare = s.rsplit('.').next().unwrap_or(&s).to_string();
696                // v7.39 (read01 regproc.c) — regtype canonicalizes the
697                // name ('int4' → 'integer') and rejects unknown types
698                // (PG 42704).
699                if matches!(target, CastTarget::RegType) {
700                    // v7.39 (round 648) — carry the OID as well as the
701                    // name, the way `::regclass` and `::regproc` already
702                    // do. As a plain Text this rendered correctly and
703                    // then failed everything downstream: `'text'::regtype
704                    // ::oid` parsed the NAME as a number and answered
705                    // `invalid input syntax for type oid: "text"` where
706                    // PG answers 25, and `pg_typeof` said `text`.
707                    return match crate::conversions::regtype_canonical_name(&bare) {
708                        Some(c) => {
709                            let oid =
710                                crate::conversions::regtype_name_to_oid(&c.to_ascii_lowercase())
711                                    .unwrap_or(0);
712                            Ok(Value::RegType(oid, c.into_boxed_str()))
713                        }
714                        None => Err(EvalError::TypeMismatch {
715                            detail: alloc::format!("type \"{s}\" does not exist"),
716                        }),
717                    };
718                }
719                // 7.38.1 S5.1 — a CATALOG relation name folds to the
720                // dual (oid, name) value, so `'pg_amop'::regclass`
721                // compares with pg_depend's numeric classid and still
722                // renders as the name (PG's regclass IS an oid). User
723                // relations keep the textual round-trip contract.
724                if let Some((_, oid)) = crate::system_catalog::CATALOG_RELATIONS
725                    .iter()
726                    .find(|(n, _)| bare.eq_ignore_ascii_case(n))
727                {
728                    return Ok(Value::RegClass(*oid, bare.into_boxed_str()));
729                }
730                Ok(Value::text(bare))
731            }
732            // A numeric OID → its type name for `::regtype` (the common
733            // `atttypid::regtype` column-type-name shape). `::regclass`
734            // needs a catalog reverse-lookup for user relations, which
735            // this cast has no access to, so it keeps rendering the OID.
736            Value::Int(_) | Value::BigInt(_) => {
737                let n = match v {
738                    Value::Int(n) => i64::from(n),
739                    Value::BigInt(n) => n,
740                    _ => unreachable!(),
741                };
742                if matches!(target, CastTarget::RegType)
743                    && let Some(name) = crate::conversions::regtype_oid_to_name_owned(n)
744                {
745                    Ok(Value::RegType(n, name.into_boxed_str()))
746                } else {
747                    Ok(Value::text(alloc::format!("{n}")))
748                }
749            }
750            other => Err(EvalError::TypeMismatch {
751                detail: alloc::format!(
752                    "::regtype / ::regclass accepts TEXT (name) or integer (oid), got {}",
753                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
754                ),
755            }),
756        },
757        // v7.10.11 — `::TEXT[]`. Decode PG external array form
758        // when input is Text; pass through unchanged when it is
759        // already TextArray. Anything else is a type mismatch.
760        CastTarget::TextArray => match v {
761            Value::TextArray(items) => Ok(Value::TextArray(items)),
762            Value::Text(s) => {
763                if let Some(r) = try_cast_2d_array(&s, |row| {
764                    decode_text_array_external(row).map(Value::TextArray)
765                }) {
766                    return r;
767                }
768                decode_text_array_external(&s).map(Value::TextArray)
769            }
770            // Other scalar arrays cast element-wise, each element
771            // rendered as its own text (NULLs preserved). PG allows
772            // `ARRAY[1,2,3]::text[]`.
773            Value::IntArray(items) => Ok(Value::TextArray(
774                items
775                    .into_iter()
776                    .map(|o| o.map(|n| alloc::format!("{n}")))
777                    .collect(),
778            )),
779            Value::BigIntArray(items) => Ok(Value::TextArray(
780                items
781                    .into_iter()
782                    .map(|o| o.map(|n| alloc::format!("{n}")))
783                    .collect(),
784            )),
785            Value::SmallIntArray(items) => Ok(Value::TextArray(
786                items
787                    .into_iter()
788                    .map(|o| o.map(|n| alloc::format!("{n}")))
789                    .collect(),
790            )),
791            Value::BoolArray(items) => Ok(Value::TextArray(
792                items
793                    .into_iter()
794                    .map(|o| o.map(|b| String::from(if b { "t" } else { "f" })))
795                    .collect(),
796            )),
797            Value::FloatArray(items) => Ok(Value::TextArray(
798                items
799                    .into_iter()
800                    .map(|o| o.map(|x| value_to_text(&Value::Float(x))))
801                    .collect(),
802            )),
803            other => Err(EvalError::TypeMismatch {
804                detail: alloc::format!(
805                    "::TEXT[] only accepts TEXT / array inputs, got {}",
806                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
807                ),
808            }),
809        },
810        // v7.11.13 — `::INT[]` / `::BIGINT[]`. Decode PG external
811        // form `{1,2,3}` when input is Text; widen TextArray /
812        // IntArray as appropriate.
813        CastTarget::IntArray => cast_to_int_array(v),
814        CastTarget::BigIntArray => cast_to_bigint_array(v),
815        // v7.12.0 — `::tsvector` / `::tsquery`. Decodes PG external
816        // form when input is Text; passes through unchanged when the
817        // input is already the target type. Other inputs are a type
818        // mismatch. Lexer / Porter stemmer arrive in v7.12.1; the
819        // external-form cast at v7.12.0 is the path pg_dump and
820        // direct-literal callers use.
821        CastTarget::TsVector => match v {
822            Value::TsVector(items) => Ok(Value::TsVector(items)),
823            Value::Text(s) => decode_tsvector_external(&s).map(Value::TsVector),
824            other => Err(EvalError::TypeMismatch {
825                detail: alloc::format!(
826                    "::tsvector only accepts TEXT / tsvector inputs, got {}",
827                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
828                ),
829            }),
830        },
831        CastTarget::TsQuery => match v {
832            Value::TsQuery(ast) => Ok(Value::TsQuery(ast)),
833            Value::Text(s) => decode_tsquery_external(&s).map(Value::TsQuery),
834            other => Err(EvalError::TypeMismatch {
835                detail: alloc::format!(
836                    "::tsquery only accepts TEXT / tsquery inputs, got {}",
837                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
838                ),
839            }),
840        },
841        // v7.17.0 — `::uuid`. Identity for `uuid → uuid`; parse
842        // text via the shared `parse_uuid_str`. Anything else is a
843        // type mismatch — PG also rejects e.g. INT → UUID without
844        // an explicit text bridge.
845        CastTarget::Uuid => match v {
846            Value::Uuid(b) => Ok(Value::Uuid(b)),
847            Value::Text(s) => match spg_storage::parse_uuid_str(&s) {
848                Some(b) => Ok(Value::Uuid(b)),
849                None => Err(EvalError::TypeMismatch {
850                    detail: alloc::format!("invalid input syntax for type uuid: {s:?}"),
851                }),
852            },
853            other => Err(EvalError::TypeMismatch {
854                detail: alloc::format!(
855                    "::uuid only accepts TEXT / uuid inputs, got {}",
856                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
857                ),
858            }),
859        },
860        // v7.18 — `::bytea`. Identity for `Bytes → Bytes`; decode
861        // Text via the engine's PG-format bytea decoder (`\x`
862        // hex form + `\NNN` escape form). Anything else is a type
863        // mismatch — same shape as PG's contract. Closes the
864        // mailrs D-pre #3 reverse-acceptance gap.
865        CastTarget::Bytea => match v {
866            Value::Bytes(b) => Ok(Value::bytes(b)),
867            Value::Text(s) => match crate::conversions::decode_bytea_literal(&s) {
868                Ok(b) => Ok(Value::bytes(b)),
869                Err(msg) => Err(EvalError::TypeMismatch {
870                    detail: alloc::format!("invalid input syntax for type bytea: {msg}"),
871                }),
872            },
873            // v7.39 (round 544) — an integer's two's-complement bytes,
874            // big-endian, at the source type's width. Measured on PG18:
875            // 5::int2 -> \x0005, 5::int4 -> \x00000005,
876            // 5::int8 -> \x0000000000000005, (-1)::int4 -> \xffffffff.
877            Value::SmallInt(n) => Ok(Value::bytes(n.to_be_bytes().to_vec())),
878            Value::Int(n) => Ok(Value::bytes(n.to_be_bytes().to_vec())),
879            Value::BigInt(n) => Ok(Value::bytes(n.to_be_bytes().to_vec())),
880            other => Err(EvalError::TypeMismatch {
881                detail: alloc::format!(
882                    "::bytea only accepts TEXT / bytea / integer inputs, got {}",
883                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
884                ),
885            }),
886        },
887        CastTarget::Named(name) => {
888            // v7.39 (round 777, F31-E1) — a typmod'd ARRAY cast:
889            // `::numeric(3,1)[]` arrives as Named("numeric(3,1)_array")
890            // and fell through to the user-type lookup ('type
891            // "numeric(3,1)_array" does not exist'). PG applies the
892            // modifier per element ({1.5, 2.3}, measured). Cast to the
893            // bare base array first, then run every element through the
894            // scalar typmod cast.
895            if let Some(base_paren) = name.strip_suffix("_array")
896                && base_paren.ends_with(')')
897                && let Some(popen) = base_paren.find('(')
898            {
899                let base = &base_paren[..popen];
900                let arr = cast_value_ref_in(
901                    v,
902                    &CastTarget::Named(alloc::format!("{base}_array")),
903                    mysql,
904                )?;
905                let scalar = CastTarget::Named(alloc::string::String::from(base_paren));
906                return match arr {
907                    Value::NumericArray(items) => {
908                        let mut out = alloc::vec::Vec::with_capacity(items.len());
909                        for it in items {
910                            out.push(match it {
911                                None => None,
912                                Some((scaled, scale)) => {
913                                    match cast_value_ref_in(
914                                        Value::Numeric { scaled, scale, kind: spg_storage::NumericKind::Finite },
915                                        &scalar,
916                                        mysql,
917                                    )? {
918                                        Value::Numeric { scaled, scale, .. } => {
919                                            Some((scaled, scale))
920                                        }
921                                        Value::NumericBig(b) => {
922                                            return Err(EvalError::TypeMismatch {
923                                                detail: alloc::format!(
924                                                    "numeric value too large for {base_paren}[]: {b:?}"
925                                                ),
926                                            });
927                                        }
928                                        Value::Null => None,
929                                        other => {
930                                            return Err(EvalError::TypeMismatch {
931                                                detail: alloc::format!(
932                                                    "unexpected element cast result {other:?}"
933                                                ),
934                                            });
935                                        }
936                                    }
937                                }
938                            });
939                        }
940                        Ok(Value::NumericArray(out))
941                    }
942                    other => Ok(other),
943                };
944            }
945            // v7.39 (round 613) — a plain scalar spelling goes straight to
946            // the tail. See `PLAIN_NAMED_TARGETS` for why that is the same
947            // thing as walking the arm, and the pin for the check that says
948            // so mechanically.
949            if let Some(dt) = plain_named_target(name) {
950                return finish_named_cast(v, dt, name, None, mysql);
951            }
952            // v7.38 (read01) — a temporal type with a fractional-seconds
953            // precision (`time(3)`, `timestamp(0)`, `timestamptz(2)`) rounds the
954            // sub-second field to that many digits, like PG. Resolve against the
955            // base type (`type_name_to_data_type` does not know the `(N)` form)
956            // and round the coerced result below.
957            // v7.38 (read01, T20) — an integer casts to `bit(n)` as the low n
958            // bits of its two's-complement representation (PG; int→varbit is
959            // rejected there, so only fixed-length `bit` is handled here).
960            if matches!(v, Value::Int(_) | Value::BigInt(_) | Value::SmallInt(_)) {
961                if let Some(width) = bit_cast_width(name) {
962                    return int_to_bit_string(v, width.0);
963                }
964            }
965            // v7.39 (read01 varbit.c) — internal exact-length form for
966            // B'...' literals (an explicit ::bit means bit(1) below).
967            if name == "__bit_literal" {
968                return match &v {
969                    Value::Null => Ok(Value::Null),
970                    Value::Text(s) => match crate::conversions::parse_bit_string_text(s) {
971                        Some((nb, by)) => Ok(Value::bit_string(nb, by)),
972                        None => Err(EvalError::TypeMismatch {
973                            detail: alloc::format!("invalid input syntax for type bit: \"{s}\""),
974                        }),
975                    },
976                    Value::BitString { .. } => Ok(v),
977                    other => Err(EvalError::TypeMismatch {
978                        detail: alloc::format!(
979                            "cannot cast {} to bit",
980                            crate::conversions::pg_type_name_for_error_opt(other.data_type())
981                        ),
982                    }),
983                };
984            }
985            // v7.39 (read01 varbit.c) — `bit(n)` over a bit string (or a
986            // '0101' text form) zero-extends on the RIGHT or truncates to
987            // n (PG's bit() cast, unlike the input-time exact-length rule).
988            let bit_src: Option<Value<'static>> = match &v {
989                Value::BitString { .. } => Some(v.clone()),
990                Value::Text(s) if bit_cast_width(name).is_some() => {
991                    match crate::conversions::parse_bit_string_text(s) {
992                        Some((nb, by)) => Some(Value::bit_string(nb, by)),
993                        None => {
994                            let bad = s.chars().find(|c| *c != '0' && *c != '1');
995                            return Err(EvalError::TypeMismatch {
996                                detail: match bad {
997                                    Some(c) => {
998                                        alloc::format!("\"{c}\" is not a valid binary digit")
999                                    }
1000                                    None => {
1001                                        alloc::format!("invalid input syntax for type bit: \"{s}\"")
1002                                    }
1003                                },
1004                            });
1005                        }
1006                    }
1007                }
1008                _ => None,
1009            };
1010            if let Some(Value::BitString { nbits, bytes }) = &bit_src {
1011                if let Some((width, pads)) = bit_cast_width(name) {
1012                    // varbit truncates but never pads.
1013                    if !pads && *nbits <= width {
1014                        return Ok(Value::BitString {
1015                            nbits: *nbits,
1016                            bytes: alloc::borrow::Cow::Owned(bytes.to_vec()),
1017                        });
1018                    }
1019                    let mut bits: alloc::vec::Vec<bool> = (0..*nbits as usize)
1020                        .map(|i| bytes[i / 8] & (0x80 >> (i % 8)) != 0)
1021                        .collect();
1022                    bits.resize(width as usize, false);
1023                    let mut out = alloc::vec![0u8; width.div_ceil(8) as usize];
1024                    for (i, b) in bits.iter().enumerate() {
1025                        if *b {
1026                            out[i / 8] |= 0x80 >> (i % 8);
1027                        }
1028                    }
1029                    return Ok(Value::BitString {
1030                        nbits: width,
1031                        bytes: alloc::borrow::Cow::Owned(out),
1032                    });
1033                }
1034            }
1035            // v7.39 (read01 oid.c) — OID is unsigned 32-bit: a negative
1036            // integer wraps (PG's (Oid) cast semantics: -1 -> 4294967295),
1037            // beyond u32 errors "OID out of range", bad text is 22P02.
1038            // v7.39 (read01 oid.c) — OID is unsigned 32-bit: a negative
1039            // integer wraps (PG's (Oid) cast semantics: -1 -> 4294967295),
1040            // beyond u32 errors "OID out of range", bad text is 22P02.
1041            //
1042            // Round 667 moved the rules to `conversions::coerce_to_oid` so
1043            // the column-assignment path shares them instead of growing a
1044            // second copy.
1045            if name.eq_ignore_ascii_case("oid")
1046                && let Some(out) = crate::conversions::coerce_to_oid(&v)?
1047            {
1048                return Ok(out);
1049            }
1050            // v7.39 (read01 mac8.c) — macaddr8 -> macaddr requires the
1051            // EUI-64 ff:fe infix; anything else is PG's dedicated error.
1052            if name.eq_ignore_ascii_case("macaddr") {
1053                if let Value::Macaddr8(b) = &v {
1054                    if b[3] == 0xff && b[4] == 0xfe {
1055                        return Ok(Value::Macaddr([b[0], b[1], b[2], b[5], b[6], b[7]]));
1056                    }
1057                    return Err(EvalError::TypeMismatch {
1058                        detail: "macaddr8 data out of range to convert to macaddr".into(),
1059                    });
1060                }
1061            }
1062            // v7.39 (read01 regproc.c) — the remaining reg* input types.
1063            // SPG carries them as their canonical text rendering; name
1064            // resolution runs against the static pg_proc table / the FTS
1065            // configuration list.
1066            // v7.39 (round 607) — matched against the static list rather than
1067            // through an owned lowercase copy. The copy was built for every
1068            // row and thrown away on every row that is not one of these.
1069            if let Some(lower_name) = REG_MISC_TYPES
1070                .iter()
1071                .copied()
1072                .find(|k| name.eq_ignore_ascii_case(k))
1073            {
1074                let s = match &v {
1075                    Value::Null => return Ok(Value::Null),
1076                    Value::Text(s) => s.as_ref().trim().to_string(),
1077                    // v7.39 (round 634) — an OID reaches these types too.
1078                    // PG registers int2/int4/int8/oid -> regproc as IMPLICIT
1079                    // casts and renders an oid with no matching entry as the
1080                    // number itself: `1::INT::REGPROC` is `1`, and
1081                    // `1247::OID::REGPROC` is `1247`. SPG refused the whole
1082                    // integer family with "accepts TEXT".
1083                    Value::SmallInt(n) => return Ok(Value::text(n.to_string())),
1084                    Value::Int(n) => return Ok(Value::text(n.to_string())),
1085                    Value::BigInt(n) => return Ok(Value::text(n.to_string())),
1086                    other => {
1087                        return Err(EvalError::TypeMismatch {
1088                            detail: alloc::format!(
1089                                "::{lower_name} accepts TEXT, got {}",
1090                                crate::conversions::pg_type_name_for_error_opt(other.data_type())
1091                            ),
1092                        });
1093                    }
1094                };
1095                return cast_reg_misc(lower_name, &s);
1096            }
1097            // v7.39 (round 514) — the remaining catalog-shaped types. Each
1098            // validates its own text form and keeps it, which is what PG's
1099            // input functions do; the wordings below are PG18 readings.
1100            if let Some(out) = cast_catalog_scalar(name, &v)? {
1101                return Ok(out);
1102            }
1103            // v7.39 (round 511) — `'(0,1)'::tid`, so a caller can name a row
1104            // it read a ctid from earlier. PG's text form is the only input
1105            // shape it has.
1106            if name.eq_ignore_ascii_case("tid") {
1107                return match &v {
1108                    Value::Tid(..) => Ok(v),
1109                    Value::Text(t) => parse_tid_text(t).ok_or_else(|| EvalError::TypeMismatch {
1110                        detail: alloc::format!("invalid input syntax for type tid: \"{t}\""),
1111                    }),
1112                    other => Err(EvalError::TypeMismatch {
1113                        detail: alloc::format!(
1114                            "cannot cast type {} to tid",
1115                            crate::eval::strings::pg_typeof_name(other)
1116                        ),
1117                    }),
1118                };
1119            }
1120            // v7.39 (read01 pseudotypes.c) — casting a value INTO a
1121            // pseudotype hits PG's dummy input functions (0A000).
1122            if let Some(lower) = OPAQUE_TYPES
1123                .iter()
1124                .copied()
1125                .find(|k| name.eq_ignore_ascii_case(k))
1126            {
1127                // v7.39 (round 509) — a pseudotype is a REAL type name,
1128                // so `NULL::anyarray` is NULL on PG, not an error. Only a
1129                // VALUE hits the dummy input function. Before this the
1130                // NULL case fell through to the type table below, which
1131                // does not carry the pseudotypes, and once NULL stopped
1132                // short-circuiting the whole cast it started reporting
1133                // them as unknown types.
1134                return if matches!(v, Value::Null) {
1135                    Ok(Value::Null)
1136                } else {
1137                    Err(EvalError::TypeMismatch {
1138                        detail: alloc::format!("cannot accept a value of type {lower}"),
1139                    })
1140                };
1141            }
1142            // v7.39 (read01 pseudotypes.c) — `::cstring` is PG's I/O-form
1143            // pseudotype: text in, text out (cstring_in/out are identity).
1144            // SPG carries it as text; pg_typeof(cstring) reading "text" is
1145            // a recorded delta (RD-1) alongside the literal projection OIDs.
1146            // v7.39 (read01 xid8funcs.c) — `::xid` (32-bit, wrapping) and
1147            // `::xid8` (64-bit, full) parse an integer text and render it
1148            // back verbatim. SPG carries them as BigInt.
1149            if name.eq_ignore_ascii_case("xid") || name.eq_ignore_ascii_case("xid8") {
1150                return Ok(match v {
1151                    Value::Null => Value::Null,
1152                    // v7.38.19 (RD-11) — PostgreSQL refuses EVERY integer
1153                    // type here, for both names, and SPG accepted them.
1154                    // Measured on PG 18.4:
1155                    //
1156                    //   SELECT 1::xid8            cannot cast type integer to xid8
1157                    //   SELECT 1::bigint::xid8    cannot cast type bigint to xid8
1158                    //   SELECT 1::bigint::xid     cannot cast type bigint to xid
1159                    //   SELECT '1'::text::xid8    1
1160                    //
1161                    // Only the text form is a cast; the integer form is
1162                    // not, because a transaction id is not a number you
1163                    // may arrive at by arithmetic.
1164                    //
1165                    // This is the direction that matters most: SPG
1166                    // accepting what PG rejects means code PG would have
1167                    // stopped runs here, and the difference surfaces
1168                    // somewhere else, later. It was found by re-measuring
1169                    // the recorded delta two lines above -- which had
1170                    // never mentioned it.
1171                    Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) => {
1172                        return Err(EvalError::TypeMismatch {
1173                            detail: alloc::format!(
1174                                "cannot cast type {} to {}",
1175                                match v {
1176                                    Value::SmallInt(_) => "smallint",
1177                                    Value::Int(_) => "integer",
1178                                    _ => "bigint",
1179                                },
1180                                name.to_ascii_lowercase()
1181                            ),
1182                        });
1183                    }
1184                    Value::Text(s) => {
1185                        let t = s.trim();
1186                        match t.parse::<u64>() {
1187                            Ok(n) => Value::BigInt(n as i64),
1188                            Err(_) => {
1189                                return Err(EvalError::TypeMismatch {
1190                                    detail: alloc::format!(
1191                                        "invalid input syntax for type {}: \"{s}\"",
1192                                        name.to_ascii_lowercase()
1193                                    ),
1194                                });
1195                            }
1196                        }
1197                    }
1198                    other => {
1199                        return Err(EvalError::TypeMismatch {
1200                            detail: alloc::format!(
1201                                "cannot cast {} to {name}",
1202                                crate::conversions::pg_type_name_for_error_opt(other.data_type())
1203                            ),
1204                        });
1205                    }
1206                });
1207            }
1208            // v7.39 (read01 varchar.c) — `::name` is text truncated to
1209            // NAMEDATALEN-1 (63) bytes.
1210            if name.eq_ignore_ascii_case("name") {
1211                return Ok(match v {
1212                    Value::Null => Value::Null,
1213                    other => {
1214                        let t = match other {
1215                            Value::Text(s) => s.into_owned(),
1216                            o => value_to_text(&o),
1217                        };
1218                        let mut cut = t;
1219                        if cut.len() > 63 {
1220                            let mut idx = 63;
1221                            while !cut.is_char_boundary(idx) {
1222                                idx -= 1;
1223                            }
1224                            cut.truncate(idx);
1225                        }
1226                        Value::text(cut)
1227                    }
1228                });
1229            }
1230            if name.eq_ignore_ascii_case("cstring") {
1231                return Ok(match v {
1232                    Value::Null => Value::Null,
1233                    Value::Text(s) => Value::Text(s),
1234                    other => Value::text(value_to_text(&other)),
1235                });
1236            }
1237            // v7.39 (read01 jsonpath.c) — `::jsonpath` parses and prints
1238            // the canonical form (PG's jsonpath type; SPG carries it as
1239            // text — the wire OID is a recorded residual with the other
1240            // literal projection OIDs).
1241            if name.eq_ignore_ascii_case("jsonpath") {
1242                return match v {
1243                    Value::Null => Ok(Value::Null),
1244                    Value::Text(s) => Ok(Value::text(crate::json::jsonpath_canonical(s.as_ref())?)),
1245                    other => Err(EvalError::TypeMismatch {
1246                        detail: alloc::format!(
1247                            "cannot cast {} to jsonpath",
1248                            crate::conversions::pg_type_name_for_error_opt(other.data_type())
1249                        ),
1250                    }),
1251                };
1252            }
1253            // v7.39 (round 355, M13) — MySQL's `BINARY` / `BINARY(n)`.
1254            // It is a COLLATION coercion, not a type change: MariaDB
1255            // renders `BINARY 'abc'` as `abc` (and `HEX()` of it as
1256            // 616263), so the value passes through unchanged; `(n)`
1257            // truncates to n bytes (`CAST('abc' AS BINARY(2))` is `ab`,
1258            // measured). What it really buys is byte-wise comparison,
1259            // which `compare_is_case_insensitive` now refuses to fold.
1260            if mysql
1261                && (name.eq_ignore_ascii_case("binary")
1262                    || name.to_ascii_lowercase().starts_with("binary("))
1263            {
1264                return cast_mysql_binary(v, name);
1265            }
1266            // v7.39 (round 352, M8) — MySQL's SIGNED / UNSIGNED targets.
1267            // Measured on MariaDB 11: a string gives its LEADING number
1268            // (`'12abc'` → 12, `'abc'` → 0); a fractional value ROUNDS
1269            // half-away-from-zero (1.5 → 2, 2.5 → 3, -2.5 → -3) rather
1270            // than truncating; and UNSIGNED wraps a negative through u64
1271            // (`-1` → 18446744073709551615).
1272            // PG has no such type — `type "signed" does not exist` — so the
1273            // reading is gated on the dialect, not just on the spelling.
1274            if mysql
1275                && (name.eq_ignore_ascii_case("signed") || name.eq_ignore_ascii_case("unsigned"))
1276            {
1277                return cast_mysql_integer(v, name.eq_ignore_ascii_case("unsigned"));
1278            }
1279            // v7.39 (round 423) — a bare MySQL temporal type carries
1280            // fractional precision 0, so `CAST(x AS DATETIME)` drops the
1281            // fraction (measured on MariaDB 11). PG's `::timestamp` keeps
1282            // every microsecond, so the default is dialect-gated.
1283            let temporal_prec = temporal_typmod(name)
1284                .or_else(|| (mysql && is_bare_temporal_type(name)).then_some(0));
1285            let resolve_name: alloc::borrow::Cow<'_, str> = if temporal_prec.is_some() {
1286                alloc::borrow::Cow::Owned(name.split('(').next().unwrap_or(name).trim().to_string())
1287            } else {
1288                alloc::borrow::Cow::Borrowed(name.as_str())
1289            };
1290            // v7.37.5 ship triage — generic typed-cast dispatch.
1291            // Resolve the ident to a `DataType` and route the value
1292            // through the existing `coerce_value` text-decoder for
1293            // every v7.37.5 γ/δ/ε/ζ-A type that already speaks
1294            // Text→typed via codec.
1295            let dt =
1296                crate::conversions::type_name_to_data_type(&resolve_name).ok_or_else(|| {
1297                    // v7.39 (round 272) — a numeric typmod outside PG's
1298                    // bounds gets PG's own wording rather than being
1299                    // reported as an unknown type.
1300                    // v7.39 (round 620) — and an unknown one is PG's
1301                    // wording, which also earns it PG's SQLSTATE (42704
1302                    // UNDEFINED_OBJECT; `unsupported cast target` fell
1303                    // through to the generic 42000).
1304                    EvalError::TypeMismatch {
1305                        detail: crate::conversions::numeric_typmod_error(&resolve_name)
1306                            .unwrap_or_else(|| unknown_type_error_text(name)),
1307                    }
1308                })?;
1309            finish_named_cast(v, dt, &resolve_name, temporal_prec, mysql)
1310        }
1311    }
1312}
1313
1314/// v7.39 (round 613) — the tail of the `Named` arm: stringify for the text
1315/// targets, coerce, and round a temporal precision. Split out so the fast
1316/// path below reaches exactly this code rather than a copy of it.
1317/// v7.39 (round 722) — the compiled `Step::CastPlain` entry: same tail,
1318/// name pre-resolved at compile time.
1319pub(crate) fn finish_named_cast_plain(
1320    v: Value<'static>,
1321    dt: spg_storage::DataType,
1322    resolve_name: &str,
1323    mysql: bool,
1324) -> Result<Value<'static>, EvalError> {
1325    finish_named_cast(v, dt, resolve_name, None, mysql)
1326}
1327
1328fn finish_named_cast(
1329    v: Value<'static>,
1330    dt: spg_storage::DataType,
1331    resolve_name: &str,
1332    temporal_prec: Option<u8>,
1333    mysql: bool,
1334) -> Result<Value<'static>, EvalError> {
1335    // PG semantics: any value casts to varchar(n) / char(n) through its text
1336    // representation (`99::char(2)` → '99'), and an EXPLICIT cast truncates
1337    // to n characters — only column assignment errors on overflow. Stringify
1338    // a non-text source first, then truncate up front so the coerce path's
1339    // length contract never fires here.
1340    let v = match (&dt, v) {
1341        // v7.38 (read01) — an explicit cast to TEXT stringifies any
1342        // value (`text(42)` → '42'), matching `42::text`. (coerce_value
1343        // deliberately rejects a bare INT→TEXT so INSERT stays strict.)
1344        (spg_storage::DataType::Text, v) => match v {
1345            Value::Text(s) => Value::Text(s),
1346            // v7.39 (read01 inet family) — inet/cidr ::text carries
1347            // the mask even at full length (cast-path form).
1348            Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
1349                Value::text(crate::conversions::format_inet_full(family, bits, &addr))
1350            }
1351            other => Value::text(value_to_text(&other)),
1352        },
1353        (spg_storage::DataType::Varchar(n) | spg_storage::DataType::Char(n), v) => {
1354            // v7.37 D.36 — previously only `Value::Text` was handled, so
1355            // `99::char(2)` reached coerce_value as an INT and hit a
1356            // CHAR/INT storage type-mismatch.
1357            // v7.39 (bpchar epic) — a bpchar source enters through its
1358            // text cast (trailing blanks stripped): `::varchar` keeps
1359            // the stripped form, `::char(m)` re-pads in coerce_value.
1360            let s = match v {
1361                Value::Text(s) => s.into_owned(),
1362                Value::BpChar(s) => s.trim_end_matches(' ').to_string(),
1363                other => value_to_text(&other),
1364            };
1365            let s = if *n > 0 && s.chars().count() > *n as usize {
1366                s.chars()
1367                    .take(*n as usize)
1368                    .collect::<alloc::string::String>()
1369            } else {
1370                s
1371            };
1372            Value::text(s)
1373        }
1374        (_, v) => v,
1375    };
1376    let coerced =
1377        crate::conversions::coerce_value(v, dt, resolve_name, 0).map_err(|e| match e {
1378            // v7.39 (read01 round 113) — pass an already-classed engine
1379            // error through unchanged. Re-stringifying via Display would
1380            // double the "eval: type mismatch: " class prefix (the wire
1381            // strips only the outermost one), leaking it into the message
1382            // — visible now that jsonb → numeric casts error with PG's
1383            // exact "cannot cast jsonb string to type numeric" wording.
1384            crate::EngineError::Eval(ev) => ev,
1385            // v7.39 (round 622, S05a) — `coerce_value` is the INSERT-time
1386            // COLUMN coercion, and a cast borrows it. Its rejection is
1387            // phrased for a column, so `SELECT 1::INET` answered
1388            //
1389            //   type mismatch in column "inet" (position 0): expected INET,
1390            //   got INT
1391            //
1392            // naming a column that does not exist, at a position that means
1393            // nothing, in the storage layer's own vocabulary. PG says
1394            // `cannot cast type integer to inet`. The column phrasing stays
1395            // where it belongs — an INSERT still says which column — and a
1396            // failed cast now says what it failed to cast, like every other
1397            // arm in this file already did.
1398            //
1399            // The two type names come off the error itself, which already
1400            // carries them as `DataType`. Naming them BEFORE the call — the
1401            // obvious way to write this, since the value and the target both
1402            // move into it — costs two `String`s on every SUCCESSFUL cast,
1403            // and the panel caught exactly that: `id::NUMERIC` 23.75 ->
1404            // 55.48 ms, `id::REAL` 21.55 -> 50.48. This is the same eager
1405            // error construction round 614 removed from 28 call sites,
1406            // rebuilt by hand a round later.
1407            crate::EngineError::Storage(spg_storage::StorageError::TypeMismatch {
1408                expected,
1409                actual,
1410                ..
1411            }) => EvalError::TypeMismatch {
1412                detail: alloc::format!(
1413                    "cannot cast {} to {}",
1414                    crate::conversions::pg_type_name_for_error(actual),
1415                    crate::conversions::pg_type_name_for_error(expected)
1416                ),
1417            },
1418            other => EvalError::TypeMismatch {
1419                detail: alloc::format!("{other}"),
1420            },
1421        })?;
1422    Ok(match temporal_prec {
1423        Some(prec) => round_temporal_to_precision(coerced, prec, mysql),
1424        None => coerced,
1425    })
1426}
1427
1428/// v7.39 (round 613) — the plain scalar spellings, with the type each one
1429/// resolves to.
1430///
1431/// Round 612 measured the `Named` arm re-deriving everything for every row:
1432/// `s::VARCHAR` cost 30.6 ms over 200k rows where `s::TEXT` — the identical
1433/// conversion, under a spelling the parser settles into a `CastTarget`
1434/// variant — cost 11.2, and probes split the difference across the whole arm
1435/// rather than any one place in it. These names reach the tail directly.
1436///
1437/// Both halves of that shortcut are checked mechanically by the pin, not by
1438/// eye: every entry's type is asserted to equal `type_name_to_data_type`'s
1439/// answer, and every entry is asserted absent from each arm above the
1440/// resolve (the reg-misc / catalog-scalar / opaque lists, `tid`, `xid`,
1441/// `xid8`, `jsonpath`, the MySQL `binary` / `signed` / `unsigned` names, and
1442/// the bit and temporal spellings). A name that grows a special case has to
1443/// leave this table, and the pin says so.
1444const PLAIN_NAMED_TARGETS: &[(&str, spg_storage::DataType)] = &[
1445    ("text", spg_storage::DataType::Text),
1446    ("varchar", spg_storage::DataType::Varchar(0)),
1447    ("character varying", spg_storage::DataType::Varchar(0)),
1448    (
1449        "numeric",
1450        spg_storage::DataType::Numeric {
1451            precision: 0,
1452            scale: 0,
1453        },
1454    ),
1455    (
1456        "decimal",
1457        spg_storage::DataType::Numeric {
1458            precision: 0,
1459            scale: 0,
1460        },
1461    ),
1462    ("real", spg_storage::DataType::Real),
1463    ("float4", spg_storage::DataType::Real),
1464    ("float8", spg_storage::DataType::Float),
1465    ("double precision", spg_storage::DataType::Float),
1466    ("int2", spg_storage::DataType::SmallInt),
1467    ("smallint", spg_storage::DataType::SmallInt),
1468    ("int4", spg_storage::DataType::Int),
1469    ("integer", spg_storage::DataType::Int),
1470    ("int8", spg_storage::DataType::BigInt),
1471    ("bool", spg_storage::DataType::Bool),
1472    ("boolean", spg_storage::DataType::Bool),
1473    ("date", spg_storage::DataType::Date),
1474    ("bytea", spg_storage::DataType::Bytes),
1475    ("uuid", spg_storage::DataType::Uuid),
1476];
1477
1478/// v7.39 (round 613) — the heads that may carry a typmod and are still
1479/// plain: `varchar(20)`, `char(4)`, `numeric(10,2)`. The type comes from
1480/// `type_name_to_data_type` over the WHOLE name, so the typmod is parsed
1481/// exactly where it always was; only the walk down the arm is skipped. The
1482/// pin checks each head against every arm above the resolve, and that none
1483/// of them is a bit or temporal spelling.
1484pub(crate) const PLAIN_NAMED_HEADS: &[&str] = &[
1485    "varchar",
1486    "character varying",
1487    "char",
1488    "character",
1489    "bpchar",
1490    "numeric",
1491    "decimal",
1492];
1493
1494/// The type a plain scalar spelling resolves to, or `None` when the name
1495/// needs the whole arm.
1496pub(crate) fn plain_named_target(name: &str) -> Option<spg_storage::DataType> {
1497    if let Some(dt) = PLAIN_NAMED_TARGETS
1498        .iter()
1499        .find(|(k, _)| name.eq_ignore_ascii_case(k))
1500        .map(|(_, dt)| *dt)
1501    {
1502        return Some(dt);
1503    }
1504    let head = name.split('(').next()?.trim();
1505    if name.len() == head.len()
1506        || !PLAIN_NAMED_HEADS
1507            .iter()
1508            .any(|k| head.eq_ignore_ascii_case(k))
1509    {
1510        return None;
1511    }
1512    crate::conversions::type_name_to_data_type(name)
1513}
1514
1515/// The scalar type names the `Named` arm resolves without a catalog.
1516fn is_known_scalar_name(lower: &str) -> bool {
1517    REG_MISC_TYPES.contains(&lower)
1518        || CATALOG_SCALAR_TYPES.contains(&lower)
1519        || OPAQUE_TYPES.contains(&lower)
1520        || matches!(
1521            lower,
1522            "tid"
1523                | "record"
1524                | "cstring"
1525                | "regnamespace"
1526                | "regrole"
1527                // Round 896 — the target validator runs before the arm, so
1528                // the quoted spelling has to be a known name here too or it
1529                // is rejected before the fold above ever sees it.
1530                | "regclass"
1531                | "regtype"
1532        )
1533}
1534
1535/// v7.39 (round 509) — does this name a type at all?
1536///
1537/// PG validates the cast TARGET whatever the operand is: `NULL::nosuchtype`
1538/// is an error there, not NULL. `cast_value_in` short-circuits a NULL before
1539/// it ever looks at the target, so the check has to happen in the caller —
1540/// and `eval_cast_arm` is the caller that has a catalog, which is what
1541/// enums, domains, composites and table row types need.
1542///
1543/// This lists what the `Named` arm below resolves WITHOUT a catalog. Keeping
1544/// the two in step is a real hazard: a first cut of this check missed three
1545/// live spellings — `::binary` (the MySQL prefix's desugar), a table's row
1546/// type, and the pseudotypes — and the e2e suite caught every one. It is the
1547/// check on this function.
1548/// v7.39 (round 620) — PG's wording for a cast target that names no type.
1549///
1550/// SPG said ``unsupported cast target `::nosuchtype` ``, which reads as "SPG
1551/// has not got round to that one" when what happened is that no such type
1552/// exists anywhere. PG says `type "nosuchtype" does not exist`, and because
1553/// the wire classifies by message text, saying it also moves the code off the
1554/// generic 42000 onto 42704 UNDEFINED_OBJECT.
1555pub(crate) fn unknown_type_error_text(name: &str) -> alloc::string::String {
1556    alloc::format!("type \"{name}\" does not exist")
1557}
1558
1559pub(crate) fn builtin_target_resolves(name: &str, mysql: bool) -> bool {
1560    if name == "__bit_literal" || bit_cast_width(name).is_some() {
1561        return true;
1562    }
1563    crate::conversions::with_lower_name(name, |lower| {
1564        builtin_target_resolves_lower(name, lower, mysql)
1565    })
1566}
1567
1568fn builtin_target_resolves_lower(name: &str, lower: &str, mysql: bool) -> bool {
1569    // The three families, read from the same declarations the value path
1570    // dispatches on — see their doc comment for why that matters.
1571    if is_known_scalar_name(lower) {
1572        return true;
1573    }
1574    // v7.39 (round 515) — `<element>[]`, which this parser names
1575    // `<element>_array`. PG has an array type for every scalar, so the rule
1576    // is the stem's: `NULL::cstring[]`, `NULL::aclitem[]` and
1577    // `NULL::"char"[]` all resolve there. A general rule rather than three
1578    // entries, because the next scalar added would otherwise need a fourth.
1579    if let Some(stem) = lower.strip_suffix("_array")
1580        && (is_known_scalar_name(stem)
1581            || crate::conversions::type_name_to_data_type(stem).is_some())
1582    {
1583        return true;
1584    }
1585    if mysql && matches!(lower, "binary" | "signed" | "unsigned") {
1586        return true;
1587    }
1588    let base = if temporal_typmod(name).is_some() || (mysql && is_bare_temporal_type(name)) {
1589        name.split('(').next().unwrap_or(name).trim()
1590    } else {
1591        name
1592    };
1593    crate::conversions::type_name_to_data_type(base).is_some()
1594        || crate::conversions::numeric_typmod_error(base).is_some()
1595}
1596
1597/// v7.39 (round 511) — PG's `(block,offset)` text form for a tid.
1598fn parse_tid_text(t: &str) -> Option<Value<'static>> {
1599    let inner = t.trim().strip_prefix('(')?.strip_suffix(')')?;
1600    let (b, o) = inner.split_once(',')?;
1601    Some(Value::Tid(
1602        b.trim().parse::<u32>().ok()?,
1603        o.trim().parse::<u32>().ok()?,
1604    ))
1605}
1606
1607/// v7.39 (round 514) — the catalog-shaped scalar types: the ids, the oid
1608/// vectors, an ACL item, a cursor name and a transaction snapshot.
1609///
1610/// `Some` when `name` is one of them, so the caller can fall through to
1611/// everything else. Every error wording is a PG18 reading — they differ per
1612/// type and per ELEMENT (`::oidvector` complains about `oid`,
1613/// `::int2vector` about `smallint`), which is why they are spelled out
1614/// rather than shared.
1615fn cast_catalog_scalar(name: &str, v: &Value<'_>) -> Result<Option<Value<'static>>, EvalError> {
1616    crate::conversions::with_lower_name(name, |lower| cast_catalog_scalar_lower(lower, v))
1617}
1618
1619fn cast_catalog_scalar_lower(
1620    lower: &str,
1621    v: &Value<'_>,
1622) -> Result<Option<Value<'static>>, EvalError> {
1623    // v7.39 (round 515) — `<element>[]` runs the element's own check over
1624    // each member and keeps the literal, which is what PG does: measured,
1625    // `'{a,b}'::aclitem[]` is "unrecognized key word: \"a\"".
1626    if let Some(stem) = lower.strip_suffix("_array")
1627        && (CATALOG_SCALAR_TYPES.contains(&stem) || OPAQUE_TYPES.contains(&stem))
1628    {
1629        let Value::Text(t) = v else {
1630            return Ok(None);
1631        };
1632        let body = t.trim();
1633        let inner = body
1634            .strip_prefix('{')
1635            .and_then(|b| b.strip_suffix('}'))
1636            .unwrap_or(body);
1637        for part in inner.split(',').filter(|p| !p.trim().is_empty()) {
1638            cast_catalog_scalar(stem, &Value::text(part.trim().to_string()))?;
1639        }
1640        return Ok(Some(Value::text(body.to_string())));
1641    }
1642    if !CATALOG_SCALAR_TYPES.contains(&lower) {
1643        return Ok(None);
1644    }
1645    let text = match v {
1646        Value::Text(t) => t.to_string(),
1647        Value::Cid(c) if lower == "cid" => return Ok(Some(Value::Cid(*c))),
1648        Value::Xid(x) if lower == "xid" => return Ok(Some(Value::Xid(*x))),
1649        // v7.39 (round 641) — PG has no cast between an integer and a
1650        // transaction id in either direction: `5::xid` is "cannot cast
1651        // type integer to xid" and `'5'::xid::int` is the mirror of it,
1652        // measured. The unknown-literal spelling `'5'::xid` is a
1653        // different thing — that is the type's input function, and it is
1654        // the Text arm above. Only `xid` is carved out here; `cid`,
1655        // `oid` and the vector types keep taking an integer.
1656        Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) if lower == "xid" => {
1657            return Err(EvalError::TypeMismatch {
1658                detail: alloc::format!(
1659                    "cannot cast type {} to xid",
1660                    crate::eval::strings::pg_typeof_name(v)
1661                ),
1662            });
1663        }
1664        Value::SmallInt(n) => alloc::format!("{n}"),
1665        Value::Int(n) => alloc::format!("{n}"),
1666        Value::BigInt(n) => alloc::format!("{n}"),
1667        other => {
1668            return Err(EvalError::TypeMismatch {
1669                detail: alloc::format!(
1670                    "cannot cast type {} to {lower}",
1671                    crate::eval::strings::pg_typeof_name(other)
1672                ),
1673            });
1674        }
1675    };
1676    let t = text.trim();
1677    let bad = |ty: &str, what: &str| EvalError::TypeMismatch {
1678        detail: alloc::format!("invalid input syntax for type {ty}: \"{what}\""),
1679    };
1680    let out = match lower {
1681        "cid" => Value::Cid(t.parse::<u32>().map_err(|_| bad("cid", t))?),
1682        "xid" => Value::Xid(t.parse::<u32>().map_err(|_| bad("xid", t))?),
1683        // Space-separated element lists, validated element by element and
1684        // kept in their own spelling.
1685        "oidvector" | "int2vector" => {
1686            let elem_ty = if lower == "oidvector" {
1687                "oid"
1688            } else {
1689                "smallint"
1690            };
1691            for part in t.split_whitespace() {
1692                let ok = if elem_ty == "oid" {
1693                    part.parse::<u32>().is_ok()
1694                } else {
1695                    part.parse::<i16>().is_ok()
1696                };
1697                if !ok {
1698                    return Err(bad(elem_ty, part));
1699                }
1700            }
1701            // v7.39.11 — a real vector, not the text that happens to
1702            // print the same. Through 7.39.10 this was `Value::text`,
1703            // and every array operation over a catalog vector raised.
1704            if elem_ty == "oid" {
1705                Value::OidVector(
1706                    t.split_whitespace()
1707                        .map(|p| p.parse::<u32>().unwrap_or_default())
1708                        .collect(),
1709                )
1710            } else {
1711                Value::Int2Vector(
1712                    t.split_whitespace()
1713                        .map(|p| p.parse::<i16>().unwrap_or_default())
1714                        .collect(),
1715                )
1716            }
1717        }
1718        // `grantee=privileges/grantor`, and PG checks the key word first:
1719        // anything before the `=` that is not a role name, `group` or
1720        // `user` is "unrecognized key word".
1721        "aclitem" => {
1722            let Some((who, rest)) = t.split_once('=') else {
1723                return Err(EvalError::TypeMismatch {
1724                    detail: alloc::format!("unrecognized key word: \"{t}\""),
1725                });
1726            };
1727            if !rest.contains('/') {
1728                return Err(EvalError::TypeMismatch {
1729                    detail: alloc::format!("a name must follow the \"/\" sign"),
1730                });
1731            }
1732            let _ = who;
1733            Value::text(t.to_string())
1734        }
1735        // A cursor name is just a name.
1736        "refcursor" => Value::text(t.to_string()),
1737        // `xmin:xmax:xip_list` — two numbers and a comma-separated tail.
1738        "pg_snapshot" | "txid_snapshot" => {
1739            let parts: alloc::vec::Vec<&str> = t.splitn(3, ':').collect();
1740            let shaped = parts.len() == 3
1741                && parts[0].parse::<u64>().is_ok()
1742                && parts[1].parse::<u64>().is_ok()
1743                && (parts[2].is_empty() || parts[2].split(',').all(|x| x.parse::<u64>().is_ok()));
1744            if !shaped {
1745                return Err(bad(lower, t));
1746            }
1747            Value::text(t.to_string())
1748        }
1749        // PG normalises a path on input: `$.a` reads back `$."a"`. The
1750        // engine already has the parser its operators use.
1751        "jsonpath" => Value::text(crate::json::jsonpath_canonical(t)?),
1752        _ => unreachable!("guarded above"),
1753    };
1754    Ok(Some(out))
1755}
1756
1757/// v7.38 (read01, T20) — width of a `bit` cast target: bare `bit` is `bit(1)`,
1758/// `bit(N)` is N. `None` for `varbit` / `bit varying` (PG rejects int→varbit) and
1759/// any non-bit name.
1760fn bit_cast_width(name: &str) -> Option<(u32, bool)> {
1761    crate::conversions::with_lower_name(name, bit_cast_width_lower)
1762}
1763
1764fn bit_cast_width_lower(lower: &str) -> Option<(u32, bool)> {
1765    let trimmed = lower.trim();
1766    if trimmed == "bit" {
1767        return Some((1, true));
1768    }
1769    // v7.39 (round 281) — `varbit(n)` / `bit varying(n)` adjust on an
1770    // explicit cast too, but only DOWN: PG truncates a too-long value
1771    // and leaves a shorter one alone, where `bit(n)` also pads.
1772    for (prefix, pads) in [("varbit", false), ("bit varying", false), ("bit", true)] {
1773        if let Some(rest) = trimmed.strip_prefix(prefix) {
1774            let rest = rest.trim_start();
1775            if let Some(inner) = rest.strip_prefix('(').and_then(|r| r.strip_suffix(')'))
1776                && let Ok(n) = inner.trim().parse::<u32>()
1777            {
1778                return Some((n, pads));
1779            }
1780        }
1781    }
1782    None
1783}
1784
1785/// v7.38 (read01, T20) — build a `bit(width)` value from an integer: the low
1786/// `width` bits of the two's-complement, packed MSB-first / left-aligned (the
1787/// on-wire bit layout). Widths past 64 sign-extend.
1788fn int_to_bit_string(v: Value<'static>, width: u32) -> Result<Value<'static>, EvalError> {
1789    let n: i64 = match v {
1790        Value::Int(x) => i64::from(x),
1791        Value::BigInt(x) => x,
1792        Value::SmallInt(x) => i64::from(x),
1793        _ => {
1794            return Err(EvalError::TypeMismatch {
1795                detail: "int_to_bit_string: non-integer source".into(),
1796            });
1797        }
1798    };
1799    let w = width as usize;
1800    let mut bytes = alloc::vec![0u8; w.div_ceil(8)];
1801    for i in 0..w {
1802        let p = w - 1 - i; // bit position counted from the LSB
1803        let bit = if p >= 64 {
1804            u8::from(n < 0) // sign-extend beyond the integer's width
1805        } else {
1806            ((n >> p) & 1) as u8
1807        };
1808        if bit != 0 {
1809            bytes[i / 8] |= 1 << (7 - (i % 8));
1810        }
1811    }
1812    Ok(Value::bit_string(width, bytes))
1813}
1814
1815/// Extract the fractional-seconds precision from a temporal cast name like
1816/// `time(3)` / `timestamp(0)` / `timestamptz(2)`; `None` for any non-temporal
1817/// type or a bare temporal type with no `(N)`.
1818fn temporal_typmod(name: &str) -> Option<u8> {
1819    crate::conversions::with_lower_name(name, |lower| {
1820        let (base, rest) = lower.split_once('(')?;
1821        if !matches!(
1822            base.trim(),
1823            "time" | "timetz" | "timestamp" | "timestamptz" | "datetime"
1824        ) {
1825            return None;
1826        }
1827        let digits = rest.trim_start();
1828        let end = digits
1829            .find(|c: char| !c.is_ascii_digit())
1830            .unwrap_or(digits.len());
1831        digits[..end].parse::<u8>().ok()
1832    })
1833}
1834
1835/// Round a TIME / TIMESTAMP value's microsecond field to `prec` fractional-
1836/// second digits (`prec` 0..=6), half-away-from-zero as PG's AdjustTimestamp.
1837/// v7.39 (round 423) — `truncate` selects MySQL's reduction mode. PG's
1838/// AdjustTimestamp ROUNDS half-away-from-zero (`::timestamp(1)` of `.256` is
1839/// `.3`); MariaDB TRUNCATES toward zero (`.2`, measured). Same function, one
1840/// flag, because everything else about the reduction is identical.
1841fn round_temporal_to_precision(v: Value<'static>, prec: u8, truncate: bool) -> Value<'static> {
1842    if prec >= 6 {
1843        return v;
1844    }
1845    let scale = 10i64.pow(u32::from(6 - prec));
1846    let reduce = |micros: i64| -> i64 {
1847        if truncate {
1848            // Toward zero, so a negative time-of-day loses the same digits.
1849            (micros / scale) * scale
1850        } else {
1851            let half = scale / 2;
1852            if micros >= 0 {
1853                ((micros + half) / scale) * scale
1854            } else {
1855                -(((-micros + half) / scale) * scale)
1856            }
1857        }
1858    };
1859    match v {
1860        Value::Timestamp(m) => Value::Timestamp(reduce(m)),
1861        Value::Time(m) => Value::Time(reduce(m)),
1862        other => other,
1863    }
1864}
1865
1866/// v7.39 (round 423) — is `name` a bare temporal type (no `(N)` modifier)?
1867/// MySQL gives those fractional precision ZERO — `CAST(x AS DATETIME)` drops
1868/// the fraction entirely — where PG's `::timestamp` keeps full microseconds.
1869fn is_bare_temporal_type(name: &str) -> bool {
1870    let t = name.trim();
1871    ["time", "timestamp", "datetime"]
1872        .iter()
1873        .any(|k| t.eq_ignore_ascii_case(k))
1874}
1875
1876fn cast_to_int_array(v: Value) -> Result<Value, EvalError> {
1877    match v {
1878        Value::IntArray(items) => Ok(Value::IntArray(items)),
1879        Value::BigIntArray(items) => {
1880            let mut out: Vec<Option<i32>> = Vec::with_capacity(items.len());
1881            for item in items {
1882                match item {
1883                    None => out.push(None),
1884                    Some(n) => match i32::try_from(n) {
1885                        Ok(x) => out.push(Some(x)),
1886                        Err(_) => {
1887                            return Err(EvalError::TypeMismatch {
1888                                detail: alloc::format!("::INT[] element {n} overflows i32"),
1889                            });
1890                        }
1891                    },
1892                }
1893            }
1894            Ok(Value::IntArray(out))
1895        }
1896        Value::Text(s) => {
1897            if let Some(r) = try_cast_2d_array(&s, |row| {
1898                decode_int_array_external(row).map(Value::IntArray)
1899            }) {
1900                return r;
1901            }
1902            decode_int_array_external(&s).map(Value::IntArray)
1903        }
1904        Value::TextArray(items) => {
1905            let mut out: Vec<Option<i32>> = Vec::with_capacity(items.len());
1906            for item in items {
1907                match item {
1908                    None => out.push(None),
1909                    Some(s) => match s.parse::<i32>() {
1910                        Ok(n) => out.push(Some(n)),
1911                        Err(_) => {
1912                            return Err(EvalError::TypeMismatch {
1913                                detail: alloc::format!("::INT[] cannot parse {s:?}"),
1914                            });
1915                        }
1916                    },
1917                }
1918            }
1919            Ok(Value::IntArray(out))
1920        }
1921        other => Err(EvalError::TypeMismatch {
1922            detail: alloc::format!(
1923                "::INT[] does not accept {}",
1924                crate::conversions::pg_type_name_for_error_opt(other.data_type())
1925            ),
1926        }),
1927    }
1928}
1929
1930fn cast_to_bigint_array(v: Value) -> Result<Value, EvalError> {
1931    match v {
1932        Value::BigIntArray(items) => Ok(Value::BigIntArray(items)),
1933        Value::IntArray(items) => Ok(Value::BigIntArray(
1934            items.into_iter().map(|x| x.map(i64::from)).collect(),
1935        )),
1936        Value::Text(s) => {
1937            if let Some(r) = try_cast_2d_array(&s, |row| {
1938                decode_bigint_array_external(row).map(Value::BigIntArray)
1939            }) {
1940                return r;
1941            }
1942            decode_bigint_array_external(&s).map(Value::BigIntArray)
1943        }
1944        Value::TextArray(items) => {
1945            let mut out: Vec<Option<i64>> = Vec::with_capacity(items.len());
1946            for item in items {
1947                match item {
1948                    None => out.push(None),
1949                    Some(s) => match s.parse::<i64>() {
1950                        Ok(n) => out.push(Some(n)),
1951                        Err(_) => {
1952                            return Err(EvalError::TypeMismatch {
1953                                detail: alloc::format!("::BIGINT[] cannot parse {s:?}"),
1954                            });
1955                        }
1956                    },
1957                }
1958            }
1959            Ok(Value::BigIntArray(out))
1960        }
1961        other => Err(EvalError::TypeMismatch {
1962            detail: alloc::format!(
1963                "::BIGINT[] does not accept {}",
1964                crate::conversions::pg_type_name_for_error_opt(other.data_type())
1965            ),
1966        }),
1967    }
1968}
1969
1970/// Cast a possibly-2-D array literal: parse each top-level row with `elem` (the
1971/// 1-D element decoder) and fold into a 2-D value; `None` when the literal is 1-D.
1972fn try_cast_2d_array(
1973    s: &str,
1974    elem: impl Fn(&str) -> Result<Value<'static>, EvalError>,
1975) -> Option<Result<Value<'static>, EvalError>> {
1976    let rows = crate::eval::values::split_2d_rows(s)?;
1977    let mut row_vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::with_capacity(rows.len());
1978    for r in &rows {
1979        match elem(r) {
1980            Ok(v) => row_vals.push(v),
1981            Err(e) => return Some(Err(e)),
1982        }
1983    }
1984    Some(
1985        crate::eval::values::build_2d_from_rows(&row_vals).ok_or_else(|| EvalError::TypeMismatch {
1986            detail: crate::conversions::malformed_array_literal(s),
1987        }),
1988    )
1989}
1990
1991fn decode_int_array_external(s: &str) -> Result<Vec<Option<i32>>, EvalError> {
1992    let trimmed = s.trim();
1993    // v7.39 (read01 jsonfuncs.c) — the json_to_record/populate desugar
1994    // routes JSON array text ("[1,2]") through this cast; accept the
1995    // bracket form alongside PG's brace form.
1996    let inner = trimmed
1997        .strip_prefix('{')
1998        .and_then(|x| x.strip_suffix('}'))
1999        .or_else(|| trimmed.strip_prefix('[').and_then(|x| x.strip_suffix(']')))
2000        .ok_or_else(|| EvalError::TypeMismatch {
2001            detail: crate::conversions::malformed_array_literal(s),
2002        })?;
2003    if inner.trim().is_empty() {
2004        return Ok(Vec::new());
2005    }
2006    inner
2007        .split(',')
2008        .map(|part| {
2009            let p = part.trim();
2010            if p.eq_ignore_ascii_case("NULL") {
2011                Ok(None)
2012            } else {
2013                p.parse::<i32>()
2014                    .map(Some)
2015                    .map_err(|_| EvalError::TypeMismatch {
2016                        detail: alloc::format!("invalid input syntax for type integer: {p:?}"),
2017                    })
2018            }
2019        })
2020        .collect()
2021}
2022
2023fn decode_bigint_array_external(s: &str) -> Result<Vec<Option<i64>>, EvalError> {
2024    let trimmed = s.trim();
2025    let inner = trimmed
2026        .strip_prefix('{')
2027        .and_then(|x| x.strip_suffix('}'))
2028        .or_else(|| trimmed.strip_prefix('[').and_then(|x| x.strip_suffix(']')))
2029        .ok_or_else(|| EvalError::TypeMismatch {
2030            // v7.39 (round 325) — was "BIGmalformed array literal", a
2031            // stray edit that shipped: the message a client saw for
2032            // `'abc'::bigint[]` began with three letters of BIGINT.
2033            detail: crate::conversions::malformed_array_literal(s),
2034        })?;
2035    if inner.trim().is_empty() {
2036        return Ok(Vec::new());
2037    }
2038    inner
2039        .split(',')
2040        .map(|part| {
2041            let p = part.trim();
2042            if p.eq_ignore_ascii_case("NULL") {
2043                Ok(None)
2044            } else {
2045                p.parse::<i64>()
2046                    .map(Some)
2047                    .map_err(|_| EvalError::TypeMismatch {
2048                        detail: alloc::format!("invalid input syntax for type bigint: {p:?}"),
2049                    })
2050            }
2051        })
2052        .collect()
2053}
2054
2055/// v7.10.11 — same decoder as `decode_text_array_literal` in
2056/// `lib.rs`, but lives here so the eval-time cast path stays
2057/// inside `spg-engine::eval`. Kept in lock-step with the engine
2058/// `coerce_value` decoder by tests.
2059fn decode_text_array_external(s: &str) -> Result<Vec<Option<String>>, EvalError> {
2060    let trimmed = s.trim();
2061    let inner = trimmed
2062        .strip_prefix('{')
2063        .and_then(|x| x.strip_suffix('}'))
2064        .or_else(|| trimmed.strip_prefix('[').and_then(|x| x.strip_suffix(']')))
2065        .ok_or_else(|| EvalError::TypeMismatch {
2066            detail: alloc::format!("TEXT[] literal {s:?} must be enclosed in '{{...}}'"),
2067        })?;
2068    let mut out: Vec<Option<String>> = Vec::new();
2069    if inner.trim().is_empty() {
2070        return Ok(out);
2071    }
2072    let bytes = inner.as_bytes();
2073    let mut i = 0;
2074    while i <= bytes.len() {
2075        while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
2076            i += 1;
2077        }
2078        if i < bytes.len() && bytes[i] == b'"' {
2079            i += 1;
2080            let mut buf = String::new();
2081            while i < bytes.len() && bytes[i] != b'"' {
2082                if bytes[i] == b'\\' && i + 1 < bytes.len() {
2083                    buf.push(bytes[i + 1] as char);
2084                    i += 2;
2085                } else {
2086                    buf.push(bytes[i] as char);
2087                    i += 1;
2088                }
2089            }
2090            if i >= bytes.len() {
2091                return Err(EvalError::TypeMismatch {
2092                    detail: "unterminated quoted element in TEXT[] literal".into(),
2093                });
2094            }
2095            i += 1;
2096            out.push(Some(buf));
2097        } else {
2098            let start = i;
2099            while i < bytes.len() && bytes[i] != b',' {
2100                i += 1;
2101            }
2102            let raw = inner[start..i].trim();
2103            if raw.eq_ignore_ascii_case("NULL") {
2104                out.push(None);
2105            } else {
2106                out.push(Some(raw.to_string()));
2107            }
2108        }
2109        while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
2110            i += 1;
2111        }
2112        if i >= bytes.len() {
2113            break;
2114        }
2115        if bytes[i] != b',' {
2116            return Err(EvalError::TypeMismatch {
2117                detail: "expected ',' between TEXT[] elements".into(),
2118            });
2119        }
2120        i += 1;
2121    }
2122    Ok(out)
2123}
2124
2125fn cast_to_interval(v: Value) -> Result<Value, EvalError> {
2126    match v {
2127        Value::Interval {
2128            months,
2129            days,
2130            micros,
2131            kind,
2132        } => Ok(Value::Interval {
2133            months,
2134            days,
2135            micros,
2136            kind,
2137        }),
2138        Value::Text(s) => {
2139            let (months, days, micros) =
2140                spg_sql::parser::parse_interval_text(&s).ok_or_else(|| {
2141                    EvalError::TypeMismatch {
2142                        // v7.39 (round 324, V42) — PG's wording.
2143                        detail: alloc::format!("invalid input syntax for type interval: \"{s}\""),
2144                    }
2145                })?;
2146            Ok(Value::Interval {
2147                months,
2148                days,
2149                micros,
2150                // v7.38.19 — the parser answers an infinity as the same
2151                // three extreme fields PostgreSQL puts on the wire, so
2152                // nothing here has to know the spelling.
2153                kind: spg_storage::IntervalKind::from_fields(months, days, micros),
2154            })
2155        }
2156        other => Err(EvalError::TypeMismatch {
2157            detail: alloc::format!(
2158                "::INTERVAL only accepts TEXT-shape inputs, got {}",
2159                crate::conversions::pg_type_name_for_error_opt(other.data_type())
2160            ),
2161        }),
2162    }
2163}
2164
2165fn cast_to_date(v: Value) -> Result<Value, EvalError> {
2166    match v {
2167        Value::Date(d) => Ok(Value::Date(d)),
2168        // Integer literals carry days since the Unix epoch — used by
2169        // the `CURRENT_DATE` AST rewrite to inject the wall clock.
2170        Value::Int(n) => Ok(Value::Date(n)),
2171        Value::BigInt(n) => {
2172            i32::try_from(n)
2173                .map(Value::Date)
2174                .map_err(|_| EvalError::TypeMismatch {
2175                    detail: "bigint days-since-epoch out of DATE range".into(),
2176                })
2177        }
2178        // Timestamp truncates to its day boundary.
2179        Value::Timestamp(t) => {
2180            let days = t.div_euclid(86_400_000_000);
2181            i32::try_from(days)
2182                .map(Value::Date)
2183                .map_err(|_| EvalError::TypeMismatch {
2184                    detail: "timestamp out of DATE range".into(),
2185                })
2186        }
2187        Value::Text(s) => {
2188            if let Some(d) = parse_date_literal(&s) {
2189                return Ok(Value::Date(d));
2190            }
2191            // PG accepts a full timestamp string in a DATE cast and
2192            // truncates to the day (verified vs live PG18.4:
2193            // `'2020-01-01 12:00:00'::date` → 2020-01-01; a bad time
2194            // like `'... 25:00:00'` still raises). Reuse the timestamp
2195            // parser — it validates the time-of-day + optional TZ — then
2196            // floor to the date via the same path as the Timestamp arm.
2197            if let Some(t) = parse_timestamp_literal(&s) {
2198                let days = t.div_euclid(86_400_000_000);
2199                return i32::try_from(days)
2200                    .map(Value::Date)
2201                    .map_err(|_| EvalError::TypeMismatch {
2202                        detail: "timestamp out of DATE range".into(),
2203                    });
2204            }
2205            // PG error split: numeric-shaped input whose field values
2206            // fail the calendar checks is "out of range" (plus PG's
2207            // DateStyle hint); anything else is an input-syntax error.
2208            if super::format::date_text_is_field_shaped(&s) {
2209                return Err(EvalError::TypeMismatch {
2210                    detail: format!(
2211                        "date/time field value out of range: {s:?}\n\
2212                         HINT:  Perhaps you need a different \"DateStyle\" setting."
2213                    ),
2214                });
2215            }
2216            Err(EvalError::TypeMismatch {
2217                detail: format!("invalid input syntax for type date: {s:?}"),
2218            })
2219        }
2220        other => Err(EvalError::TypeMismatch {
2221            detail: format!(
2222                "cannot cast {} to DATE",
2223                crate::conversions::pg_type_name_for_error_opt(other.data_type())
2224            ),
2225        }),
2226    }
2227}
2228
2229fn cast_to_timestamp(v: Value) -> Result<Value, EvalError> {
2230    match v {
2231        Value::Timestamp(t) => Ok(Value::Timestamp(t)),
2232        // Int / BigInt carry microseconds since the Unix epoch — used
2233        // by the `NOW()` / `CURRENT_TIMESTAMP` AST rewrite to inject
2234        // the wall clock as a plain integer literal.
2235        Value::Int(n) => Ok(Value::Timestamp(i64::from(n))),
2236        Value::BigInt(n) => Ok(Value::Timestamp(n)),
2237        // DATE → TIMESTAMP picks midnight on the date.
2238        // v7.39 (read01 timestamp.c) — sentinel-aware (the plain multiply
2239        // overflowed on ±infinity dates).
2240        Value::Date(d) => Ok(Value::Timestamp(crate::conversions::date_days_to_micros(d))),
2241        Value::Text(s) => {
2242            // v7.39 (round 289) — the target has no zone, so PG ignores
2243            // any the literal carries: `'…+02'::timestamp` keeps the
2244            // wall clock rather than converting to UTC.
2245            crate::eval::format::parse_timestamp_literal_wall_ordered(
2246                &s,
2247                crate::eval::format::DateOrder::Mdy,
2248            )
2249            .map(Value::Timestamp)
2250            .ok_or_else(|| EvalError::TypeMismatch {
2251                // v7.39 (round 324, V42) — PG's wording, and PG's split
2252                // between "invalid input syntax" and "date/time field
2253                // value out of range".
2254                detail: crate::eval::format::datetime_input_error_text(&s, "timestamp"),
2255            })
2256        }
2257        other => Err(EvalError::TypeMismatch {
2258            detail: format!(
2259                "cannot cast {} to TIMESTAMP",
2260                crate::conversions::pg_type_name_for_error_opt(other.data_type())
2261            ),
2262        }),
2263    }
2264}
2265
2266/// v7.39 (round 310) — `::timestamptz` from text: an offset in the
2267/// literal is APPLIED, unlike the zone-less sibling which discards it.
2268/// Naive input (no offset) is read as UTC, which is what the
2269/// context-aware arm already assumed when it fell through to here.
2270fn cast_to_timestamptz(v: Value) -> Result<Value, EvalError> {
2271    let Value::Text(s) = &v else {
2272        return cast_to_timestamp(v);
2273    };
2274    crate::eval::format::parse_timestamp_literal_tz_ordered(s, crate::eval::format::DateOrder::Mdy)
2275        .map(|(micros, _had_tz)| Value::Timestamp(micros))
2276        .ok_or_else(|| EvalError::TypeMismatch {
2277            // v7.39 (round 324, V42) — and with the RIGHT type name: this arm
2278            // used to report `TIMESTAMP` for a `::timestamptz` cast.
2279            detail: crate::eval::format::datetime_input_error_text(s, "timestamp with time zone"),
2280        })
2281}
2282
2283/// v7.39 (round 254) — PG refuses to cast a NUMERIC special into any
2284/// integer type: `cannot convert NaN to integer` / `cannot convert
2285/// infinity to bigint` (an infinity is named without its sign, probed
2286/// live). Returns `None` for an ordinary value so the caller runs its
2287/// normal conversion.
2288fn cast_numeric_special_reject(
2289    v: &Value,
2290    target: &str,
2291) -> Option<Result<Value<'static>, EvalError>> {
2292    let Value::Numeric { kind, .. } = v else {
2293        return None;
2294    };
2295    if *kind == spg_storage::NumericKind::Finite {
2296        return None;
2297    }
2298    let what = if *kind == spg_storage::NumericKind::NaN {
2299        "NaN"
2300    } else {
2301        "infinity"
2302    };
2303    Some(Err(EvalError::TypeMismatch {
2304        detail: alloc::format!("cannot convert {what} to {target}"),
2305    }))
2306}
2307
2308fn cast_numeric_to_int(v: Value) -> Result<Value, EvalError> {
2309    match v {
2310        // v7.39 (round 633) — SMALLINT. `1::SMALLINT::INT` answered
2311        // "cannot cast smallint to int": the arm was simply absent, next to
2312        // the Int and BigInt ones. Widening a smallint is about as ordinary
2313        // as a cast gets, and PG has it registered as an IMPLICIT cast.
2314        // Same omission shape as the sum accumulator missing SmallInt in
2315        // round 626 — a variant list written out by hand, one entry short.
2316        Value::SmallInt(n) => Ok(Value::Int(i32::from(n))),
2317        Value::Int(n) => Ok(Value::Int(n)),
2318        Value::BigInt(n) => i32::try_from(n)
2319            .map(Value::Int)
2320            // v7.39 (read01 round 79) — PG's wording, which the Float arm two
2321            // arms down was already using: "integer out of range". Drivers match
2322            // on it. Three arms of one function had two different messages.
2323            .map_err(|_| EvalError::TypeMismatch {
2324                detail: "integer out of range".into(),
2325            }),
2326        // PG rounds (half-to-even) coercing a real number to an integer, and
2327        // errors on a non-finite or out-of-range value (`'inf'::int`,
2328        // `1e20::int`) rather than saturating.
2329        #[allow(clippy::cast_possible_truncation)]
2330        Value::Float(x) => {
2331            let r = f64_round_half_even(x);
2332            if !r.is_finite() || !(-2_147_483_648.0..=2_147_483_647.0).contains(&r) {
2333                return Err(EvalError::TypeMismatch {
2334                    detail: "integer out of range".into(),
2335                });
2336            }
2337            Ok(Value::Int(r as i32))
2338        }
2339        // v7.39 (read01 round 112) — `real` (float4) rounds/range-checks the
2340        // same way float8 does; only the float8 arm existed.
2341        #[allow(clippy::cast_possible_truncation)]
2342        Value::Real(x) => {
2343            let r = f64_round_half_even(f64::from(x));
2344            if !r.is_finite() || !(-2_147_483_648.0..=2_147_483_647.0).contains(&r) {
2345                return Err(EvalError::TypeMismatch {
2346                    detail: "integer out of range".into(),
2347                });
2348            }
2349            Ok(Value::Int(r as i32))
2350        }
2351        Value::Numeric { scaled, scale, .. } => {
2352            let rounded = numeric_round_to_i128(scaled, scale);
2353            i32::try_from(rounded)
2354                .map(Value::Int)
2355                .map_err(|_| EvalError::TypeMismatch {
2356                    detail: "integer out of range".into(),
2357                })
2358        }
2359        Value::Text(s) => crate::conversions::parse_pg_int(&s)
2360            .and_then(|n| i32::try_from(n).ok())
2361            .map(Value::Int)
2362            .ok_or_else(|| EvalError::TypeMismatch {
2363                detail: format!("invalid input syntax for type integer: {s:?}"),
2364            }),
2365        Value::Bool(b) => Ok(Value::Int(i32::from(b))),
2366        // v7.39 (read01 char.c) — ("char")::int is the byte value.
2367        Value::Char1(b) => Ok(Value::Int(i32::from(b))),
2368        // PG `bit`/`varbit` → int is the MSB-first bit value.
2369        #[allow(clippy::cast_possible_truncation)]
2370        Value::BitString { nbits, bytes } => Ok(Value::Int(crate::conversions::bit_string_to_i64(
2371            nbits, &bytes,
2372        ) as i32)),
2373        // v7.39 (read01 round 113) — jsonb → int: decode the JSON scalar, then
2374        // round via the numeric arm above. String/array/object/boolean error.
2375        Value::Json(s) => match crate::conversions::jsonb_scalar_for_cast(&s, "integer")? {
2376            crate::conversions::JsonbScalar::Numeric(n) => cast_numeric_to_int(n),
2377            crate::conversions::JsonbScalar::Bool(_) => Err(
2378                crate::conversions::jsonb_cast_type_error("boolean", "integer"),
2379            ),
2380            crate::conversions::JsonbScalar::Null => Ok(Value::Null),
2381        },
2382        other => Err(EvalError::TypeMismatch {
2383            detail: format!(
2384                "cannot cast {} to int",
2385                crate::conversions::pg_type_name_for_error_opt(other.data_type())
2386            ),
2387        }),
2388    }
2389}
2390
2391fn cast_numeric_to_bigint(v: Value) -> Result<Value, EvalError> {
2392    match v {
2393        Value::Int(n) => Ok(Value::BigInt(i64::from(n))),
2394        // v7.39 (round 633) — SMALLINT, missing here for the same reason.
2395        Value::SmallInt(n) => Ok(Value::BigInt(i64::from(n))),
2396        Value::BigInt(n) => Ok(Value::BigInt(n)),
2397        // PG rounds (half-to-even) coercing a real number to bigint, and errors
2398        // on a non-finite or out-of-range value rather than saturating.
2399        #[allow(clippy::cast_possible_truncation)]
2400        Value::Float(x) => {
2401            let r = f64_round_half_even(x);
2402            if !r.is_finite()
2403                || !(-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&r)
2404            {
2405                return Err(EvalError::TypeMismatch {
2406                    detail: "bigint out of range".into(),
2407                });
2408            }
2409            Ok(Value::BigInt(r as i64))
2410        }
2411        // v7.39 (read01 round 112) — `real` (float4) → bigint, matching float8.
2412        #[allow(clippy::cast_possible_truncation)]
2413        Value::Real(x) => {
2414            let r = f64_round_half_even(f64::from(x));
2415            if !r.is_finite()
2416                || !(-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&r)
2417            {
2418                return Err(EvalError::TypeMismatch {
2419                    detail: "bigint out of range".into(),
2420                });
2421            }
2422            Ok(Value::BigInt(r as i64))
2423        }
2424        Value::Numeric { scaled, scale, .. } => {
2425            let rounded = numeric_round_to_i128(scaled, scale);
2426            i64::try_from(rounded)
2427                .map(Value::BigInt)
2428                .map_err(|_| EvalError::TypeMismatch {
2429                    detail: format!("numeric {rounded} does not fit in bigint"),
2430                })
2431        }
2432        Value::Text(s) => crate::conversions::parse_pg_int(&s)
2433            .map(Value::BigInt)
2434            .ok_or_else(|| EvalError::TypeMismatch {
2435                // v7.39 (round 324, V42) — PG's wording.
2436                detail: format!("invalid input syntax for type bigint: \"{s}\""),
2437            }),
2438        Value::Bool(b) => Ok(Value::BigInt(i64::from(b))),
2439        // PG `bit`/`varbit` → bigint is the MSB-first bit value.
2440        Value::BitString { nbits, bytes } => Ok(Value::BigInt(
2441            crate::conversions::bit_string_to_i64(nbits, &bytes),
2442        )),
2443        // v7.39 (read01 round 113) — jsonb → bigint.
2444        Value::Json(s) => match crate::conversions::jsonb_scalar_for_cast(&s, "bigint")? {
2445            crate::conversions::JsonbScalar::Numeric(n) => cast_numeric_to_bigint(n),
2446            crate::conversions::JsonbScalar::Bool(_) => Err(
2447                crate::conversions::jsonb_cast_type_error("boolean", "bigint"),
2448            ),
2449            crate::conversions::JsonbScalar::Null => Ok(Value::Null),
2450        },
2451        other => Err(EvalError::TypeMismatch {
2452            detail: format!(
2453                "cannot cast {} to bigint",
2454                crate::conversions::pg_type_name_for_error_opt(other.data_type())
2455            ),
2456        }),
2457    }
2458}
2459
2460fn cast_numeric_to_float(v: Value) -> Result<Value, EvalError> {
2461    match v {
2462        Value::Int(n) => Ok(Value::Float(f64::from(n))),
2463        #[allow(clippy::cast_precision_loss)]
2464        Value::BigInt(n) => Ok(Value::Float(n as f64)),
2465        Value::Float(x) => Ok(Value::Float(x)),
2466        // PG's numeric→double precision is an implicit cast; a
2467        // `Value::Numeric` (from `::numeric`, a numeric column, or
2468        // numeric arithmetic) must convert to f64, not error.
2469        #[allow(clippy::cast_precision_loss)]
2470        // v7.39 (round 254) — a special crosses to its IEEE twin.
2471        Value::Numeric { kind, .. } if kind != spg_storage::NumericKind::Finite => {
2472            Ok(Value::Float(match kind {
2473                spg_storage::NumericKind::NaN => f64::NAN,
2474                spg_storage::NumericKind::PosInf => f64::INFINITY,
2475                _ => f64::NEG_INFINITY,
2476            }))
2477        }
2478        Value::Numeric { scaled, scale, .. } => Ok(Value::Float(
2479            (scaled as f64) / f64_powi(10.0, i32::from(scale)),
2480        )),
2481        Value::Text(s) => {
2482            let t = s.trim();
2483            // Unparseable → invalid syntax; parseable-but-out-of-range (overflow
2484            // to ±∞ / nonzero underflow to 0) → out of range, the way PG's
2485            // float8in does, rather than silently yielding Infinity/0. Shared
2486            // with the Named-cast coerce path so `::float` and `::float8` agree.
2487            if t.parse::<f64>().is_err() {
2488                return Err(EvalError::TypeMismatch {
2489                    detail: format!("cannot parse {s:?} as float"),
2490                });
2491            }
2492            crate::conversions::parse_float8(t)
2493                .map(Value::Float)
2494                .ok_or_else(|| EvalError::TypeMismatch {
2495                    detail: format!("\"{t}\" is out of range for type double precision"),
2496                })
2497        }
2498        // v7.39 (read01 round 113) — jsonb → double precision.
2499        Value::Json(s) => {
2500            match crate::conversions::jsonb_scalar_for_cast(&s, "double precision")? {
2501                crate::conversions::JsonbScalar::Numeric(n) => cast_numeric_to_float(n),
2502                crate::conversions::JsonbScalar::Bool(_) => Err(
2503                    crate::conversions::jsonb_cast_type_error("boolean", "double precision"),
2504                ),
2505                crate::conversions::JsonbScalar::Null => Ok(Value::Null),
2506            }
2507        }
2508        other => Err(EvalError::TypeMismatch {
2509            detail: format!(
2510                "cannot cast {} to float",
2511                crate::conversions::pg_type_name_for_error_opt(other.data_type())
2512            ),
2513        }),
2514    }
2515}
2516
2517fn cast_to_bool(v: Value) -> Result<Value, EvalError> {
2518    match v {
2519        Value::Bool(b) => Ok(Value::Bool(b)),
2520        Value::Int(n) => Ok(Value::Bool(n != 0)),
2521        Value::BigInt(n) => Ok(Value::Bool(n != 0)),
2522        Value::Text(s) => {
2523            // PG boolin accepts any unambiguous prefix of true/false/yes/no
2524            // plus on/off/1/0 (case-insensitive, trimmed); `o` alone is
2525            // ambiguous (on vs off) and errors.
2526            let lo = s.trim().to_ascii_lowercase();
2527            match lo.as_str() {
2528                "1" | "t" | "tr" | "tru" | "true" | "y" | "ye" | "yes" | "on" => {
2529                    Ok(Value::Bool(true))
2530                }
2531                "0" | "f" | "fa" | "fal" | "fals" | "false" | "n" | "no" | "of" | "off" => {
2532                    Ok(Value::Bool(false))
2533                }
2534                _ => Err(EvalError::TypeMismatch {
2535                    detail: format!("invalid input syntax for type boolean: {:?}", s.trim()),
2536                }),
2537            }
2538        }
2539        // v7.39 (read01 round 113) — jsonb → boolean accepts only JSON
2540        // true/false; a JSON number/string/array/object errors.
2541        Value::Json(s) => match crate::conversions::jsonb_scalar_for_cast(&s, "boolean")? {
2542            crate::conversions::JsonbScalar::Bool(b) => Ok(Value::Bool(b)),
2543            crate::conversions::JsonbScalar::Numeric(_) => Err(
2544                crate::conversions::jsonb_cast_type_error("numeric", "boolean"),
2545            ),
2546            crate::conversions::JsonbScalar::Null => Ok(Value::Null),
2547        },
2548        other => Err(EvalError::TypeMismatch {
2549            detail: format!(
2550                "cannot cast {} to bool",
2551                crate::conversions::pg_type_name_for_error_opt(other.data_type())
2552            ),
2553        }),
2554    }
2555}
2556
2557/// Parse a `Value::text("[1.0, 2.0, 3.0]")` into a `Value::vector(..)`. Mirrors
2558/// pgvector's `'[..]'::vector` cast. NULL casts as NULL.
2559pub fn cast_to_vector(v: Value) -> Result<Value<'static>, EvalError> {
2560    match v {
2561        Value::Null => Ok(Value::Null),
2562        Value::Vector(v) => Ok(Value::vector(v.into_owned())),
2563        Value::Text(s) => {
2564            parse_vector_text(&s)
2565                .map(Value::vector)
2566                .ok_or_else(|| EvalError::TypeMismatch {
2567                    detail: format!("cannot parse {s:?} as a vector literal"),
2568                })
2569        }
2570        other => Err(EvalError::TypeMismatch {
2571            detail: format!(
2572                "::vector requires text input, got {}",
2573                crate::conversions::pg_type_name_for_error_opt(other.data_type())
2574            ),
2575        }),
2576    }
2577}
2578
2579/// Parse `"[1.0, 2.0, -3]"` into `Vec<f32>`. Returns `None` on malformed input.
2580pub fn parse_vector_text(s: &str) -> Option<Vec<f32>> {
2581    let trimmed = s.trim();
2582    let inner = trimmed.strip_prefix('[')?.strip_suffix(']')?;
2583    let trimmed_inner = inner.trim();
2584    if trimmed_inner.is_empty() {
2585        return Some(Vec::new());
2586    }
2587    let mut out = Vec::new();
2588    for part in trimmed_inner.split(',') {
2589        let f: f32 = part.trim().parse().ok()?;
2590        out.push(f);
2591    }
2592    Some(out)
2593}
2594
2595#[cfg(test)]
2596mod round613_plain_named_targets {
2597    use super::*;
2598
2599    /// v7.39 (round 613) — the shortcut is only equivalent to walking the
2600    /// arm while these hold. Checked here rather than by eye, so a name that
2601    /// grows a special case above the resolve fails the gate instead of
2602    /// silently taking the wrong path.
2603    fn assert_no_arm_above_the_resolve_claims(name: &str) {
2604        assert!(
2605            !REG_MISC_TYPES.iter().any(|k| name.eq_ignore_ascii_case(k)),
2606            "{name} is a reg-misc type"
2607        );
2608        assert!(
2609            !CATALOG_SCALAR_TYPES
2610                .iter()
2611                .any(|k| name.eq_ignore_ascii_case(k)),
2612            "{name} is a catalog scalar"
2613        );
2614        assert!(
2615            !OPAQUE_TYPES.iter().any(|k| name.eq_ignore_ascii_case(k)),
2616            "{name} is a pseudotype"
2617        );
2618        for special in [
2619            "__bit_literal",
2620            "tid",
2621            "xid",
2622            "xid8",
2623            "jsonpath",
2624            "binary",
2625            "signed",
2626            "unsigned",
2627        ] {
2628            assert!(
2629                !name.eq_ignore_ascii_case(special),
2630                "{name} has its own arm ({special})"
2631            );
2632        }
2633        assert!(bit_cast_width(name).is_none(), "{name} is a bit spelling");
2634        assert!(
2635            temporal_typmod(name).is_none(),
2636            "{name} carries a temporal precision"
2637        );
2638        assert!(
2639            !is_bare_temporal_type(name),
2640            "{name} is a bare temporal type"
2641        );
2642        assert!(
2643            matches!(cast_catalog_scalar(name, &Value::text("x")), Ok(None)),
2644            "{name} is claimed by the catalog-scalar arm"
2645        );
2646    }
2647
2648    #[test]
2649    fn every_plain_target_resolves_to_the_type_the_table_claims() {
2650        for (name, dt) in PLAIN_NAMED_TARGETS {
2651            assert_eq!(
2652                crate::conversions::type_name_to_data_type(name),
2653                Some(*dt),
2654                "{name} does not resolve to the type the table gives it"
2655            );
2656            assert_eq!(plain_named_target(name), Some(*dt));
2657            // The spelling is matched without regard to case.
2658            assert_eq!(plain_named_target(&name.to_uppercase()), Some(*dt));
2659            assert_no_arm_above_the_resolve_claims(name);
2660        }
2661    }
2662
2663    #[test]
2664    fn every_typmod_head_is_plain_and_resolves_through_the_type_table() {
2665        for head in PLAIN_NAMED_HEADS {
2666            assert_no_arm_above_the_resolve_claims(head);
2667            for spelled in [alloc::format!("{head}(4)"), alloc::format!("{head}(10,2)")] {
2668                assert_no_arm_above_the_resolve_claims(&spelled);
2669                assert_eq!(
2670                    plain_named_target(&spelled),
2671                    crate::conversions::type_name_to_data_type(&spelled),
2672                    "{spelled} takes a different type through the shortcut"
2673                );
2674            }
2675            // A bare head with no typmod only shortcuts when it is in the
2676            // exact table; the head list alone must not claim it.
2677            let bare = plain_named_target(head);
2678            let exact = PLAIN_NAMED_TARGETS
2679                .iter()
2680                .find(|(k, _)| head.eq_ignore_ascii_case(k))
2681                .map(|(_, dt)| *dt);
2682            assert_eq!(bare, exact, "{head} bare");
2683        }
2684    }
2685
2686    #[test]
2687    fn a_name_with_its_own_arm_is_not_shortcut() {
2688        for name in [
2689            "regproc",
2690            "aclitem",
2691            "anyarray",
2692            "tid",
2693            "xid",
2694            "jsonpath",
2695            "bit",
2696            "bit(4)",
2697            "timestamp",
2698            "timestamp(2)",
2699            "time(3)",
2700            "nosuchtype",
2701            "int4range",
2702        ] {
2703            assert_eq!(plain_named_target(name), None, "{name} was shortcut");
2704        }
2705    }
2706}