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