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