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