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