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