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