use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use spg_sql::ast::CastTarget;
use spg_storage::Value;
use super::math::{f64_powi, f64_round_half_even};
use super::{
EvalError, decode_tsquery_external, decode_tsvector_external, parse_date_literal,
parse_timestamp_literal, value_to_text,
};
pub(crate) const REG_MISC_TYPES: &[&str] = &[
"regproc",
"regprocedure",
"regoper",
"regoperator",
"regconfig",
"regdictionary",
"regcollation",
];
pub(crate) const CATALOG_SCALAR_TYPES: &[&str] = &[
"cid",
"xid",
"oidvector",
"int2vector",
"aclitem",
"refcursor",
"pg_snapshot",
"txid_snapshot",
"jsonpath",
];
pub(crate) const OPAQUE_TYPES: &[&str] = &[
"anyarray",
"anyelement",
"anyenum",
"anyrange",
"anymultirange",
"anynonarray",
"anycompatible",
"anycompatiblearray",
"anycompatiblenonarray",
"anycompatiblerange",
"anycompatiblemultirange",
"any",
"trigger",
"event_trigger",
"internal",
"language_handler",
"fdw_handler",
"pg_ddl_command",
"pg_node_tree",
"pg_ndistinct",
"pg_mcv_list",
"pg_dependencies",
"pg_brin_minmax_multi_summary",
"pg_brin_bloom_summary",
"gtsvector",
];
fn cast_reg_misc(kind: &str, s: &str) -> Result<Value<'static>, EvalError> {
let bare = s
.strip_prefix("pg_catalog.")
.unwrap_or(s)
.trim()
.to_string();
match kind {
"regconfig" => {
const CONFIGS: &[&str] = &[
"simple",
"arabic",
"armenian",
"basque",
"catalan",
"danish",
"dutch",
"english",
"finnish",
"french",
"german",
"greek",
"hindi",
"hungarian",
"indonesian",
"irish",
"italian",
"lithuanian",
"nepali",
"norwegian",
"portuguese",
"romanian",
"russian",
"serbian",
"spanish",
"swedish",
"tamil",
"turkish",
"yiddish",
];
if CONFIGS.contains(&bare.as_str()) {
Ok(Value::text(bare))
} else {
Err(EvalError::TypeMismatch {
detail: alloc::format!("text search configuration \"{s}\" does not exist"),
})
}
}
"regcollation" => {
let quoted = bare.starts_with('"') && bare.ends_with('"') && bare.len() >= 2;
let name = if quoted {
bare[1..bare.len() - 1].to_string()
} else {
bare.to_ascii_lowercase()
};
const COLLATIONS: &[&str] = &["C", "POSIX", "default", "ucs_basic"];
match COLLATIONS.iter().find(|c| **c == name) {
Some(c) => Ok(Value::text(
if c.chars().all(|ch| ch.is_ascii_lowercase() || ch == '_') && *c != "default" {
(*c).to_string()
} else {
alloc::format!("\"{c}\"")
},
)),
None => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"collation \"{name}\" for encoding \"UTF8\" does not exist"
),
}),
}
}
"regdictionary" => {
if bare == "simple" || bare.ends_with("_stem") {
Ok(Value::text(bare))
} else {
Err(EvalError::TypeMismatch {
detail: alloc::format!("text search dictionary \"{s}\" does not exist"),
})
}
}
"regproc" => {
let hits = crate::system_catalog::PG_PROC_FUNCS
.iter()
.filter(|(_, n, ..)| *n == bare)
.count();
match hits {
0 => Err(EvalError::TypeMismatch {
detail: alloc::format!("function \"{s}\" does not exist"),
}),
1 => Ok(Value::text(bare)),
_ => Err(EvalError::TypeMismatch {
detail: alloc::format!("more than one function named \"{bare}\""),
}),
}
}
"regprocedure" => {
let Some((fname, rest)) = bare.split_once('(') else {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("expected a left parenthesis in \"{s}\""),
});
};
let Some(args_txt) = rest.strip_suffix(')') else {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("expected a right parenthesis in \"{s}\""),
});
};
let fname = fname.trim().to_ascii_lowercase();
let args: Vec<String> = if args_txt.trim().is_empty() {
Vec::new()
} else {
args_txt
.split(',')
.map(|a| {
crate::conversions::regtype_canonical_name(a.trim()).ok_or_else(|| {
EvalError::TypeMismatch {
detail: alloc::format!("type \"{}\" does not exist", a.trim()),
}
})
})
.collect::<Result<_, _>>()?
};
let nargs = args.len() as i32;
let known = crate::system_catalog::PG_PROC_FUNCS
.iter()
.any(|(_, n, _, na, _)| *n == fname && *na == nargs);
if !known {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("function \"{s}\" does not exist"),
});
}
Ok(Value::text(alloc::format!("{fname}({})", args.join(","))))
}
_ => {
let sym: String = bare.chars().filter(|c| !c.is_whitespace()).collect();
let core_op = !sym.is_empty() && sym.chars().all(|c| "+-*/<>=~!@#%^&|`?".contains(c));
if core_op {
Err(EvalError::TypeMismatch {
detail: alloc::format!("more than one operator named {sym}"),
})
} else {
Err(EvalError::TypeMismatch {
detail: alloc::format!("operator does not exist: {s}"),
})
}
}
}
}
fn cast_mysql_binary(v: Value<'static>, name: &str) -> Result<Value<'static>, EvalError> {
let limit: Option<usize> = name
.split_once('(')
.and_then(|(_, rest)| rest.trim_end_matches(')').trim().parse().ok());
let text = match &v {
Value::Null => return Ok(Value::Null),
Value::Text(t) => t.to_string(),
Value::BpChar(t) => t.to_string(),
other => crate::eval::values::value_to_text(other),
};
Ok(Value::text(match limit {
Some(n) if text.len() > n => {
let mut cut = n;
while cut > 0 && !text.is_char_boundary(cut) {
cut -= 1;
}
text[..cut].to_string()
}
_ => text,
}))
}
fn cast_mysql_integer(v: Value<'static>, unsigned: bool) -> Result<Value<'static>, EvalError> {
let exact: Option<i128> = match &v {
Value::Bool(b) => Some(i128::from(u8::from(*b))),
Value::SmallInt(x) => Some(i128::from(*x)),
Value::Int(x) => Some(i128::from(*x)),
Value::BigInt(x) => Some(i128::from(*x)),
Value::Numeric {
scaled,
scale: 0,
kind: spg_storage::NumericKind::Finite,
} => Some(*scaled),
_ => None,
};
let rounded: i128 = match exact {
Some(n) => n,
None => {
let n: f64 = match &v {
Value::Null => return Ok(Value::Null),
Value::Float(x) => *x,
Value::Real(x) => f64::from(*x),
#[allow(clippy::cast_precision_loss)]
Value::Numeric { scaled, scale, .. } => {
*scaled as f64 / 10_f64.powi(i32::from(*scale))
}
Value::Text(t) | Value::BpChar(t) => crate::eval::mysql_leading_number(t),
other => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"cannot cast {} to integer",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
let r = if n >= 0.0 {
(n + 0.5).floor()
} else {
(n - 0.5).ceil()
};
#[allow(clippy::cast_possible_truncation)]
let as_i64 = r as i64;
i128::from(as_i64)
}
};
if unsigned {
let wrapped: u64 = if rounded < 0 {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
{
rounded as i64 as u64
}
} else {
u64::try_from(rounded).unwrap_or(u64::MAX)
};
return Ok(if wrapped > i64::MAX as u64 {
Value::Numeric {
scaled: i128::from(wrapped),
scale: 0,
kind: spg_storage::NumericKind::Finite,
}
} else {
#[allow(clippy::cast_possible_wrap)]
Value::BigInt(wrapped as i64)
});
}
Ok(Value::BigInt(
i64::try_from(rounded).unwrap_or(if rounded < 0 { i64::MIN } else { i64::MAX }),
))
}
#[inline(never)]
fn bytea_to_integer(v: &Value<'static>, width: usize) -> Result<Value<'static>, EvalError> {
let Value::Bytes(b) = v else {
return Err(EvalError::TypeMismatch {
detail: alloc::string::String::from("expected bytea"),
});
};
if b.len() > width {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("bytea of {} bytes is too wide for the target", b.len()),
});
}
let negative = b.len() == width && b.first().is_some_and(|f| *f & 0x80 != 0);
let mut acc: i64 = if negative { -1 } else { 0 };
for byte in b.iter() {
acc = (acc << 8) | i64::from(*byte);
}
Ok(if width == 4 {
Value::Int(i32::try_from(acc).unwrap_or(0))
} else {
Value::BigInt(acc)
})
}
fn numeric_round_to_i128(scaled: i128, scale: u16) -> i128 {
let factor = 10_i128.pow(u32::from(scale));
let neg = scaled < 0;
let abs = scaled.unsigned_abs() as i128;
let q = abs / factor;
let r = abs % factor;
let mag = if 2 * r >= factor { q + 1 } else { q };
if neg { -mag } else { mag }
}
pub fn cast_value(v: Value<'static>, target: CastTarget) -> Result<Value<'static>, EvalError> {
cast_value_in(v, target, false)
}
pub fn cast_value_in(
v: Value<'static>,
target: CastTarget,
mysql: bool,
) -> Result<Value<'static>, EvalError> {
cast_value_ref_in(v, &target, mysql)
}
pub fn cast_value_ref_in(
v: Value<'static>,
target: &CastTarget,
mysql: bool,
) -> Result<Value<'static>, EvalError> {
if let CastTarget::Named(n) = target {
if n.eq_ignore_ascii_case("regclass") {
return cast_value_ref_in(v, &CastTarget::RegClass, mysql);
}
if n.eq_ignore_ascii_case("regtype") {
return cast_value_ref_in(v, &CastTarget::RegType, mysql);
}
}
if matches!(v, Value::Null) {
return Ok(Value::Null);
}
match target {
CastTarget::Vector => cast_to_vector(v),
CastTarget::Text => Ok(Value::text(match &v {
Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
crate::conversions::format_inet_full(*family, *bits, addr)
}
Value::BpChar(s) => s.trim_end_matches(' ').to_string(),
Value::RegClass(_, name) | Value::RegProc(_, name) | Value::RegType(_, name) => {
name.to_string()
}
_ => value_to_text(&v),
})),
CastTarget::BigInt | CastTarget::Int
if matches!(
v,
Value::RegClass(..) | Value::RegProc(..) | Value::RegType(..)
) =>
{
let (Value::RegClass(oid, _) | Value::RegProc(oid, _) | Value::RegType(oid, _)) = v
else {
unreachable!("guarded above")
};
Ok(if matches!(target, CastTarget::BigInt) {
Value::BigInt(oid)
} else {
Value::Int(i32::try_from(oid).unwrap_or(i32::MAX))
})
}
CastTarget::Int if matches!(v, Value::Bytes(_)) => bytea_to_integer(&v, 4),
CastTarget::BigInt if matches!(v, Value::Bytes(_)) => bytea_to_integer(&v, 8),
CastTarget::Int => {
cast_numeric_special_reject(&v, "integer").unwrap_or_else(|| cast_numeric_to_int(v))
}
CastTarget::BigInt => {
cast_numeric_special_reject(&v, "bigint").unwrap_or_else(|| cast_numeric_to_bigint(v))
}
CastTarget::Float => cast_numeric_to_float(v),
CastTarget::Bool => cast_to_bool(v),
CastTarget::Date => cast_to_date(v),
CastTarget::Timestamp => {
let out = cast_to_timestamp(v)?;
Ok(if mysql {
round_temporal_to_precision(out, 0, true)
} else {
out
})
}
CastTarget::Timestamptz => cast_to_timestamptz(v),
CastTarget::Interval => match v {
Value::Time(us) => Ok(Value::Interval {
months: 0,
days: 0,
micros: us,
}),
other => cast_to_interval(other),
},
CastTarget::Json => match v {
Value::Json(s) => Ok(Value::json(s)),
Value::Text(s) => Ok(Value::json(s)),
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::json only accepts TEXT-shape inputs, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
},
CastTarget::Jsonb => match v {
Value::Json(s) | Value::Text(s) => match crate::json::canonicalize_jsonb(s.as_ref()) {
Ok(c) => Ok(Value::json(c)),
Err(_) => Err(EvalError::TypeMismatch {
detail: alloc::string::String::from("invalid input syntax for type json"),
}),
},
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::jsonb only accepts TEXT-shape inputs, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
},
CastTarget::Named(n) if n.eq_ignore_ascii_case("regtype_array") => {
let Value::Text(s) = &v else {
return Ok(v);
};
let body = s.trim();
let inner = body
.strip_prefix('{')
.and_then(|b| b.strip_suffix('}'))
.unwrap_or(body);
let mut out: Vec<Option<alloc::string::String>> = Vec::new();
for part in inner.split(',') {
let t = part.trim();
if t.is_empty() {
continue;
}
if t.eq_ignore_ascii_case("NULL") {
out.push(None);
continue;
}
let bare = t.rsplit('.').next().unwrap_or(t);
match crate::conversions::regtype_canonical_name(bare) {
Some(c) => out.push(Some(c)),
None => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("type \"{t}\" does not exist"),
});
}
}
}
Ok(Value::TextArray(out))
}
CastTarget::RegType | CastTarget::RegClass => match v {
Value::Text(s) => {
let bare = s.rsplit('.').next().unwrap_or(&s).to_string();
if matches!(target, CastTarget::RegType) {
return match crate::conversions::regtype_canonical_name(&bare) {
Some(c) => {
let oid =
crate::conversions::regtype_name_to_oid(&c.to_ascii_lowercase())
.unwrap_or(0);
Ok(Value::RegType(oid, c.into_boxed_str()))
}
None => Err(EvalError::TypeMismatch {
detail: alloc::format!("type \"{s}\" does not exist"),
}),
};
}
Ok(Value::text(bare))
}
Value::Int(_) | Value::BigInt(_) => {
let n = match v {
Value::Int(n) => i64::from(n),
Value::BigInt(n) => n,
_ => unreachable!(),
};
if matches!(target, CastTarget::RegType)
&& let Some(name) = crate::conversions::regtype_oid_to_name_owned(n)
{
Ok(Value::RegType(n, name.into_boxed_str()))
} else {
Ok(Value::text(alloc::format!("{n}")))
}
}
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::regtype / ::regclass accepts TEXT (name) or integer (oid), got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
},
CastTarget::TextArray => match v {
Value::TextArray(items) => Ok(Value::TextArray(items)),
Value::Text(s) => {
if let Some(r) = try_cast_2d_array(&s, |row| {
decode_text_array_external(row).map(Value::TextArray)
}) {
return r;
}
decode_text_array_external(&s).map(Value::TextArray)
}
Value::IntArray(items) => Ok(Value::TextArray(
items
.into_iter()
.map(|o| o.map(|n| alloc::format!("{n}")))
.collect(),
)),
Value::BigIntArray(items) => Ok(Value::TextArray(
items
.into_iter()
.map(|o| o.map(|n| alloc::format!("{n}")))
.collect(),
)),
Value::SmallIntArray(items) => Ok(Value::TextArray(
items
.into_iter()
.map(|o| o.map(|n| alloc::format!("{n}")))
.collect(),
)),
Value::BoolArray(items) => Ok(Value::TextArray(
items
.into_iter()
.map(|o| o.map(|b| String::from(if b { "t" } else { "f" })))
.collect(),
)),
Value::FloatArray(items) => Ok(Value::TextArray(
items
.into_iter()
.map(|o| o.map(|x| value_to_text(&Value::Float(x))))
.collect(),
)),
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::TEXT[] only accepts TEXT / array inputs, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
},
CastTarget::IntArray => cast_to_int_array(v),
CastTarget::BigIntArray => cast_to_bigint_array(v),
CastTarget::TsVector => match v {
Value::TsVector(items) => Ok(Value::TsVector(items)),
Value::Text(s) => decode_tsvector_external(&s).map(Value::TsVector),
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::tsvector only accepts TEXT / tsvector inputs, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
},
CastTarget::TsQuery => match v {
Value::TsQuery(ast) => Ok(Value::TsQuery(ast)),
Value::Text(s) => decode_tsquery_external(&s).map(Value::TsQuery),
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::tsquery only accepts TEXT / tsquery inputs, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
},
CastTarget::Uuid => match v {
Value::Uuid(b) => Ok(Value::Uuid(b)),
Value::Text(s) => match spg_storage::parse_uuid_str(&s) {
Some(b) => Ok(Value::Uuid(b)),
None => Err(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type uuid: {s:?}"),
}),
},
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::uuid only accepts TEXT / uuid inputs, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
},
CastTarget::Bytea => match v {
Value::Bytes(b) => Ok(Value::bytes(b)),
Value::Text(s) => match crate::conversions::decode_bytea_literal(&s) {
Ok(b) => Ok(Value::bytes(b)),
Err(msg) => Err(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type bytea: {msg}"),
}),
},
Value::SmallInt(n) => Ok(Value::bytes(n.to_be_bytes().to_vec())),
Value::Int(n) => Ok(Value::bytes(n.to_be_bytes().to_vec())),
Value::BigInt(n) => Ok(Value::bytes(n.to_be_bytes().to_vec())),
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::bytea only accepts TEXT / bytea / integer inputs, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
},
CastTarget::Named(name) => {
if let Some(base_paren) = name.strip_suffix("_array")
&& base_paren.ends_with(')')
&& let Some(popen) = base_paren.find('(')
{
let base = &base_paren[..popen];
let arr = cast_value_ref_in(
v,
&CastTarget::Named(alloc::format!("{base}_array")),
mysql,
)?;
let scalar = CastTarget::Named(alloc::string::String::from(base_paren));
return match arr {
Value::NumericArray(items) => {
let mut out = alloc::vec::Vec::with_capacity(items.len());
for it in items {
out.push(match it {
None => None,
Some((scaled, scale)) => {
match cast_value_ref_in(
Value::Numeric { scaled, scale, kind: spg_storage::NumericKind::Finite },
&scalar,
mysql,
)? {
Value::Numeric { scaled, scale, .. } => {
Some((scaled, scale))
}
Value::NumericBig(b) => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"numeric value too large for {base_paren}[]: {b:?}"
),
});
}
Value::Null => None,
other => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"unexpected element cast result {other:?}"
),
});
}
}
}
});
}
Ok(Value::NumericArray(out))
}
other => Ok(other),
};
}
if let Some(dt) = plain_named_target(name) {
return finish_named_cast(v, dt, name, None, mysql);
}
if matches!(v, Value::Int(_) | Value::BigInt(_) | Value::SmallInt(_)) {
if let Some(width) = bit_cast_width(name) {
return int_to_bit_string(v, width.0);
}
}
if name == "__bit_literal" {
return match &v {
Value::Null => Ok(Value::Null),
Value::Text(s) => match crate::conversions::parse_bit_string_text(s) {
Some((nb, by)) => Ok(Value::bit_string(nb, by)),
None => Err(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type bit: \"{s}\""),
}),
},
Value::BitString { .. } => Ok(v),
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"cannot cast {} to bit",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
};
}
let bit_src: Option<Value<'static>> = match &v {
Value::BitString { .. } => Some(v.clone()),
Value::Text(s) if bit_cast_width(name).is_some() => {
match crate::conversions::parse_bit_string_text(s) {
Some((nb, by)) => Some(Value::bit_string(nb, by)),
None => {
let bad = s.chars().find(|c| *c != '0' && *c != '1');
return Err(EvalError::TypeMismatch {
detail: match bad {
Some(c) => {
alloc::format!("\"{c}\" is not a valid binary digit")
}
None => {
alloc::format!("invalid input syntax for type bit: \"{s}\"")
}
},
});
}
}
}
_ => None,
};
if let Some(Value::BitString { nbits, bytes }) = &bit_src {
if let Some((width, pads)) = bit_cast_width(name) {
if !pads && *nbits <= width {
return Ok(Value::BitString {
nbits: *nbits,
bytes: alloc::borrow::Cow::Owned(bytes.to_vec()),
});
}
let mut bits: alloc::vec::Vec<bool> = (0..*nbits as usize)
.map(|i| bytes[i / 8] & (0x80 >> (i % 8)) != 0)
.collect();
bits.resize(width as usize, false);
let mut out = alloc::vec![0u8; width.div_ceil(8) as usize];
for (i, b) in bits.iter().enumerate() {
if *b {
out[i / 8] |= 0x80 >> (i % 8);
}
}
return Ok(Value::BitString {
nbits: width,
bytes: alloc::borrow::Cow::Owned(out),
});
}
}
if name.eq_ignore_ascii_case("oid")
&& let Some(out) = crate::conversions::coerce_to_oid(&v)?
{
return Ok(out);
}
if name.eq_ignore_ascii_case("macaddr") {
if let Value::Macaddr8(b) = &v {
if b[3] == 0xff && b[4] == 0xfe {
return Ok(Value::Macaddr([b[0], b[1], b[2], b[5], b[6], b[7]]));
}
return Err(EvalError::TypeMismatch {
detail: "macaddr8 data out of range to convert to macaddr".into(),
});
}
}
if let Some(lower_name) = REG_MISC_TYPES
.iter()
.copied()
.find(|k| name.eq_ignore_ascii_case(k))
{
let s = match &v {
Value::Null => return Ok(Value::Null),
Value::Text(s) => s.as_ref().trim().to_string(),
Value::SmallInt(n) => return Ok(Value::text(n.to_string())),
Value::Int(n) => return Ok(Value::text(n.to_string())),
Value::BigInt(n) => return Ok(Value::text(n.to_string())),
other => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::{lower_name} accepts TEXT, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
return cast_reg_misc(lower_name, &s);
}
if let Some(out) = cast_catalog_scalar(name, &v)? {
return Ok(out);
}
if name.eq_ignore_ascii_case("tid") {
return match &v {
Value::Tid(..) => Ok(v),
Value::Text(t) => parse_tid_text(t).ok_or_else(|| EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type tid: \"{t}\""),
}),
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"cannot cast type {} to tid",
crate::eval::strings::pg_typeof_name(other)
),
}),
};
}
if let Some(lower) = OPAQUE_TYPES
.iter()
.copied()
.find(|k| name.eq_ignore_ascii_case(k))
{
return if matches!(v, Value::Null) {
Ok(Value::Null)
} else {
Err(EvalError::TypeMismatch {
detail: alloc::format!("cannot accept a value of type {lower}"),
})
};
}
if name.eq_ignore_ascii_case("xid") || name.eq_ignore_ascii_case("xid8") {
return Ok(match v {
Value::Null => Value::Null,
Value::SmallInt(n) => Value::BigInt(i64::from(n)),
Value::Int(n) => Value::BigInt(i64::from(n)),
Value::BigInt(n) => Value::BigInt(n),
Value::Text(s) => {
let t = s.trim();
match t.parse::<u64>() {
Ok(n) => Value::BigInt(n as i64),
Err(_) => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"invalid input syntax for type {}: \"{s}\"",
name.to_ascii_lowercase()
),
});
}
}
}
other => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"cannot cast {} to {name}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
});
}
if name.eq_ignore_ascii_case("name") {
return Ok(match v {
Value::Null => Value::Null,
other => {
let t = match other {
Value::Text(s) => s.into_owned(),
o => value_to_text(&o),
};
let mut cut = t;
if cut.len() > 63 {
let mut idx = 63;
while !cut.is_char_boundary(idx) {
idx -= 1;
}
cut.truncate(idx);
}
Value::text(cut)
}
});
}
if name.eq_ignore_ascii_case("cstring") {
return Ok(match v {
Value::Null => Value::Null,
Value::Text(s) => Value::Text(s),
other => Value::text(value_to_text(&other)),
});
}
if name.eq_ignore_ascii_case("jsonpath") {
return match v {
Value::Null => Ok(Value::Null),
Value::Text(s) => Ok(Value::text(crate::json::jsonpath_canonical(s.as_ref())?)),
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"cannot cast {} to jsonpath",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
};
}
if mysql
&& (name.eq_ignore_ascii_case("binary")
|| name.to_ascii_lowercase().starts_with("binary("))
{
return cast_mysql_binary(v, name);
}
if mysql
&& (name.eq_ignore_ascii_case("signed") || name.eq_ignore_ascii_case("unsigned"))
{
return cast_mysql_integer(v, name.eq_ignore_ascii_case("unsigned"));
}
let temporal_prec = temporal_typmod(name)
.or_else(|| (mysql && is_bare_temporal_type(name)).then_some(0));
let resolve_name: alloc::borrow::Cow<'_, str> = if temporal_prec.is_some() {
alloc::borrow::Cow::Owned(name.split('(').next().unwrap_or(name).trim().to_string())
} else {
alloc::borrow::Cow::Borrowed(name.as_str())
};
let dt =
crate::conversions::type_name_to_data_type(&resolve_name).ok_or_else(|| {
EvalError::TypeMismatch {
detail: crate::conversions::numeric_typmod_error(&resolve_name)
.unwrap_or_else(|| unknown_type_error_text(name)),
}
})?;
finish_named_cast(v, dt, &resolve_name, temporal_prec, mysql)
}
}
}
pub(crate) fn finish_named_cast_plain(
v: Value<'static>,
dt: spg_storage::DataType,
resolve_name: &str,
mysql: bool,
) -> Result<Value<'static>, EvalError> {
finish_named_cast(v, dt, resolve_name, None, mysql)
}
fn finish_named_cast(
v: Value<'static>,
dt: spg_storage::DataType,
resolve_name: &str,
temporal_prec: Option<u8>,
mysql: bool,
) -> Result<Value<'static>, EvalError> {
let v = match (&dt, v) {
(spg_storage::DataType::Text, v) => match v {
Value::Text(s) => Value::Text(s),
Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
Value::text(crate::conversions::format_inet_full(family, bits, &addr))
}
other => Value::text(value_to_text(&other)),
},
(spg_storage::DataType::Varchar(n) | spg_storage::DataType::Char(n), v) => {
let s = match v {
Value::Text(s) => s.into_owned(),
Value::BpChar(s) => s.trim_end_matches(' ').to_string(),
other => value_to_text(&other),
};
let s = if *n > 0 && s.chars().count() > *n as usize {
s.chars()
.take(*n as usize)
.collect::<alloc::string::String>()
} else {
s
};
Value::text(s)
}
(_, v) => v,
};
let coerced =
crate::conversions::coerce_value(v, dt, resolve_name, 0).map_err(|e| match e {
crate::EngineError::Eval(ev) => ev,
crate::EngineError::Storage(spg_storage::StorageError::TypeMismatch {
expected,
actual,
..
}) => EvalError::TypeMismatch {
detail: alloc::format!(
"cannot cast {} to {}",
crate::conversions::pg_type_name_for_error(actual),
crate::conversions::pg_type_name_for_error(expected)
),
},
other => EvalError::TypeMismatch {
detail: alloc::format!("{other}"),
},
})?;
Ok(match temporal_prec {
Some(prec) => round_temporal_to_precision(coerced, prec, mysql),
None => coerced,
})
}
const PLAIN_NAMED_TARGETS: &[(&str, spg_storage::DataType)] = &[
("text", spg_storage::DataType::Text),
("varchar", spg_storage::DataType::Varchar(0)),
("character varying", spg_storage::DataType::Varchar(0)),
(
"numeric",
spg_storage::DataType::Numeric {
precision: 0,
scale: 0,
},
),
(
"decimal",
spg_storage::DataType::Numeric {
precision: 0,
scale: 0,
},
),
("real", spg_storage::DataType::Real),
("float4", spg_storage::DataType::Real),
("float8", spg_storage::DataType::Float),
("double precision", spg_storage::DataType::Float),
("int2", spg_storage::DataType::SmallInt),
("smallint", spg_storage::DataType::SmallInt),
("int4", spg_storage::DataType::Int),
("integer", spg_storage::DataType::Int),
("int8", spg_storage::DataType::BigInt),
("bool", spg_storage::DataType::Bool),
("boolean", spg_storage::DataType::Bool),
("date", spg_storage::DataType::Date),
("bytea", spg_storage::DataType::Bytes),
("uuid", spg_storage::DataType::Uuid),
];
pub(crate) const PLAIN_NAMED_HEADS: &[&str] = &[
"varchar",
"character varying",
"char",
"character",
"bpchar",
"numeric",
"decimal",
];
pub(crate) fn plain_named_target(name: &str) -> Option<spg_storage::DataType> {
if let Some(dt) = PLAIN_NAMED_TARGETS
.iter()
.find(|(k, _)| name.eq_ignore_ascii_case(k))
.map(|(_, dt)| *dt)
{
return Some(dt);
}
let head = name.split('(').next()?.trim();
if name.len() == head.len()
|| !PLAIN_NAMED_HEADS
.iter()
.any(|k| head.eq_ignore_ascii_case(k))
{
return None;
}
crate::conversions::type_name_to_data_type(name)
}
fn is_known_scalar_name(lower: &str) -> bool {
REG_MISC_TYPES.contains(&lower)
|| CATALOG_SCALAR_TYPES.contains(&lower)
|| OPAQUE_TYPES.contains(&lower)
|| matches!(
lower,
"tid"
| "record"
| "cstring"
| "regnamespace"
| "regrole"
| "regclass"
| "regtype"
)
}
pub(crate) fn unknown_type_error_text(name: &str) -> alloc::string::String {
alloc::format!("type \"{name}\" does not exist")
}
pub(crate) fn builtin_target_resolves(name: &str, mysql: bool) -> bool {
if name == "__bit_literal" || bit_cast_width(name).is_some() {
return true;
}
crate::conversions::with_lower_name(name, |lower| {
builtin_target_resolves_lower(name, lower, mysql)
})
}
fn builtin_target_resolves_lower(name: &str, lower: &str, mysql: bool) -> bool {
if is_known_scalar_name(lower) {
return true;
}
if let Some(stem) = lower.strip_suffix("_array")
&& (is_known_scalar_name(stem)
|| crate::conversions::type_name_to_data_type(stem).is_some())
{
return true;
}
if mysql && matches!(lower, "binary" | "signed" | "unsigned") {
return true;
}
let base = if temporal_typmod(name).is_some() || (mysql && is_bare_temporal_type(name)) {
name.split('(').next().unwrap_or(name).trim()
} else {
name
};
crate::conversions::type_name_to_data_type(base).is_some()
|| crate::conversions::numeric_typmod_error(base).is_some()
}
fn parse_tid_text(t: &str) -> Option<Value<'static>> {
let inner = t.trim().strip_prefix('(')?.strip_suffix(')')?;
let (b, o) = inner.split_once(',')?;
Some(Value::Tid(
b.trim().parse::<u32>().ok()?,
o.trim().parse::<u32>().ok()?,
))
}
fn cast_catalog_scalar(name: &str, v: &Value<'_>) -> Result<Option<Value<'static>>, EvalError> {
crate::conversions::with_lower_name(name, |lower| cast_catalog_scalar_lower(lower, v))
}
fn cast_catalog_scalar_lower(
lower: &str,
v: &Value<'_>,
) -> Result<Option<Value<'static>>, EvalError> {
if let Some(stem) = lower.strip_suffix("_array")
&& (CATALOG_SCALAR_TYPES.contains(&stem) || OPAQUE_TYPES.contains(&stem))
{
let Value::Text(t) = v else {
return Ok(None);
};
let body = t.trim();
let inner = body
.strip_prefix('{')
.and_then(|b| b.strip_suffix('}'))
.unwrap_or(body);
for part in inner.split(',').filter(|p| !p.trim().is_empty()) {
cast_catalog_scalar(stem, &Value::text(part.trim().to_string()))?;
}
return Ok(Some(Value::text(body.to_string())));
}
if !CATALOG_SCALAR_TYPES.contains(&lower) {
return Ok(None);
}
let text = match v {
Value::Text(t) => t.to_string(),
Value::Cid(c) if lower == "cid" => return Ok(Some(Value::Cid(*c))),
Value::Xid(x) if lower == "xid" => return Ok(Some(Value::Xid(*x))),
Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) if lower == "xid" => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"cannot cast type {} to xid",
crate::eval::strings::pg_typeof_name(v)
),
});
}
Value::SmallInt(n) => alloc::format!("{n}"),
Value::Int(n) => alloc::format!("{n}"),
Value::BigInt(n) => alloc::format!("{n}"),
other => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"cannot cast type {} to {lower}",
crate::eval::strings::pg_typeof_name(other)
),
});
}
};
let t = text.trim();
let bad = |ty: &str, what: &str| EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type {ty}: \"{what}\""),
};
let out = match lower {
"cid" => Value::Cid(t.parse::<u32>().map_err(|_| bad("cid", t))?),
"xid" => Value::Xid(t.parse::<u32>().map_err(|_| bad("xid", t))?),
"oidvector" | "int2vector" => {
let elem_ty = if lower == "oidvector" {
"oid"
} else {
"smallint"
};
for part in t.split_whitespace() {
let ok = if elem_ty == "oid" {
part.parse::<u32>().is_ok()
} else {
part.parse::<i16>().is_ok()
};
if !ok {
return Err(bad(elem_ty, part));
}
}
Value::text(t.to_string())
}
"aclitem" => {
let Some((who, rest)) = t.split_once('=') else {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("unrecognized key word: \"{t}\""),
});
};
if !rest.contains('/') {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("a name must follow the \"/\" sign"),
});
}
let _ = who;
Value::text(t.to_string())
}
"refcursor" => Value::text(t.to_string()),
"pg_snapshot" | "txid_snapshot" => {
let parts: alloc::vec::Vec<&str> = t.splitn(3, ':').collect();
let shaped = parts.len() == 3
&& parts[0].parse::<u64>().is_ok()
&& parts[1].parse::<u64>().is_ok()
&& (parts[2].is_empty() || parts[2].split(',').all(|x| x.parse::<u64>().is_ok()));
if !shaped {
return Err(bad(lower, t));
}
Value::text(t.to_string())
}
"jsonpath" => Value::text(crate::json::jsonpath_canonical(t)?),
_ => unreachable!("guarded above"),
};
Ok(Some(out))
}
fn bit_cast_width(name: &str) -> Option<(u32, bool)> {
crate::conversions::with_lower_name(name, bit_cast_width_lower)
}
fn bit_cast_width_lower(lower: &str) -> Option<(u32, bool)> {
let trimmed = lower.trim();
if trimmed == "bit" {
return Some((1, true));
}
for (prefix, pads) in [("varbit", false), ("bit varying", false), ("bit", true)] {
if let Some(rest) = trimmed.strip_prefix(prefix) {
let rest = rest.trim_start();
if let Some(inner) = rest.strip_prefix('(').and_then(|r| r.strip_suffix(')'))
&& let Ok(n) = inner.trim().parse::<u32>()
{
return Some((n, pads));
}
}
}
None
}
fn int_to_bit_string(v: Value<'static>, width: u32) -> Result<Value<'static>, EvalError> {
let n: i64 = match v {
Value::Int(x) => i64::from(x),
Value::BigInt(x) => x,
Value::SmallInt(x) => i64::from(x),
_ => {
return Err(EvalError::TypeMismatch {
detail: "int_to_bit_string: non-integer source".into(),
});
}
};
let w = width as usize;
let mut bytes = alloc::vec![0u8; w.div_ceil(8)];
for i in 0..w {
let p = w - 1 - i; let bit = if p >= 64 {
u8::from(n < 0) } else {
((n >> p) & 1) as u8
};
if bit != 0 {
bytes[i / 8] |= 1 << (7 - (i % 8));
}
}
Ok(Value::bit_string(width, bytes))
}
fn temporal_typmod(name: &str) -> Option<u8> {
crate::conversions::with_lower_name(name, |lower| {
let (base, rest) = lower.split_once('(')?;
if !matches!(
base.trim(),
"time" | "timetz" | "timestamp" | "timestamptz" | "datetime"
) {
return None;
}
let digits = rest.trim_start();
let end = digits
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(digits.len());
digits[..end].parse::<u8>().ok()
})
}
fn round_temporal_to_precision(v: Value<'static>, prec: u8, truncate: bool) -> Value<'static> {
if prec >= 6 {
return v;
}
let scale = 10i64.pow(u32::from(6 - prec));
let reduce = |micros: i64| -> i64 {
if truncate {
(micros / scale) * scale
} else {
let half = scale / 2;
if micros >= 0 {
((micros + half) / scale) * scale
} else {
-(((-micros + half) / scale) * scale)
}
}
};
match v {
Value::Timestamp(m) => Value::Timestamp(reduce(m)),
Value::Time(m) => Value::Time(reduce(m)),
other => other,
}
}
fn is_bare_temporal_type(name: &str) -> bool {
let t = name.trim();
["time", "timestamp", "datetime"]
.iter()
.any(|k| t.eq_ignore_ascii_case(k))
}
fn cast_to_int_array(v: Value) -> Result<Value, EvalError> {
match v {
Value::IntArray(items) => Ok(Value::IntArray(items)),
Value::BigIntArray(items) => {
let mut out: Vec<Option<i32>> = Vec::with_capacity(items.len());
for item in items {
match item {
None => out.push(None),
Some(n) => match i32::try_from(n) {
Ok(x) => out.push(Some(x)),
Err(_) => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("::INT[] element {n} overflows i32"),
});
}
},
}
}
Ok(Value::IntArray(out))
}
Value::Text(s) => {
if let Some(r) = try_cast_2d_array(&s, |row| {
decode_int_array_external(row).map(Value::IntArray)
}) {
return r;
}
decode_int_array_external(&s).map(Value::IntArray)
}
Value::TextArray(items) => {
let mut out: Vec<Option<i32>> = Vec::with_capacity(items.len());
for item in items {
match item {
None => out.push(None),
Some(s) => match s.parse::<i32>() {
Ok(n) => out.push(Some(n)),
Err(_) => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("::INT[] cannot parse {s:?}"),
});
}
},
}
}
Ok(Value::IntArray(out))
}
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::INT[] does not accept {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
fn cast_to_bigint_array(v: Value) -> Result<Value, EvalError> {
match v {
Value::BigIntArray(items) => Ok(Value::BigIntArray(items)),
Value::IntArray(items) => Ok(Value::BigIntArray(
items.into_iter().map(|x| x.map(i64::from)).collect(),
)),
Value::Text(s) => {
if let Some(r) = try_cast_2d_array(&s, |row| {
decode_bigint_array_external(row).map(Value::BigIntArray)
}) {
return r;
}
decode_bigint_array_external(&s).map(Value::BigIntArray)
}
Value::TextArray(items) => {
let mut out: Vec<Option<i64>> = Vec::with_capacity(items.len());
for item in items {
match item {
None => out.push(None),
Some(s) => match s.parse::<i64>() {
Ok(n) => out.push(Some(n)),
Err(_) => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("::BIGINT[] cannot parse {s:?}"),
});
}
},
}
}
Ok(Value::BigIntArray(out))
}
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::BIGINT[] does not accept {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
fn try_cast_2d_array(
s: &str,
elem: impl Fn(&str) -> Result<Value<'static>, EvalError>,
) -> Option<Result<Value<'static>, EvalError>> {
let rows = crate::eval::values::split_2d_rows(s)?;
let mut row_vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::with_capacity(rows.len());
for r in &rows {
match elem(r) {
Ok(v) => row_vals.push(v),
Err(e) => return Some(Err(e)),
}
}
Some(
crate::eval::values::build_2d_from_rows(&row_vals).ok_or_else(|| EvalError::TypeMismatch {
detail: crate::conversions::malformed_array_literal(s),
}),
)
}
fn decode_int_array_external(s: &str) -> Result<Vec<Option<i32>>, EvalError> {
let trimmed = s.trim();
let inner = trimmed
.strip_prefix('{')
.and_then(|x| x.strip_suffix('}'))
.or_else(|| trimmed.strip_prefix('[').and_then(|x| x.strip_suffix(']')))
.ok_or_else(|| EvalError::TypeMismatch {
detail: crate::conversions::malformed_array_literal(s),
})?;
if inner.trim().is_empty() {
return Ok(Vec::new());
}
inner
.split(',')
.map(|part| {
let p = part.trim();
if p.eq_ignore_ascii_case("NULL") {
Ok(None)
} else {
p.parse::<i32>()
.map(Some)
.map_err(|_| EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type integer: {p:?}"),
})
}
})
.collect()
}
fn decode_bigint_array_external(s: &str) -> Result<Vec<Option<i64>>, EvalError> {
let trimmed = s.trim();
let inner = trimmed
.strip_prefix('{')
.and_then(|x| x.strip_suffix('}'))
.or_else(|| trimmed.strip_prefix('[').and_then(|x| x.strip_suffix(']')))
.ok_or_else(|| EvalError::TypeMismatch {
detail: crate::conversions::malformed_array_literal(s),
})?;
if inner.trim().is_empty() {
return Ok(Vec::new());
}
inner
.split(',')
.map(|part| {
let p = part.trim();
if p.eq_ignore_ascii_case("NULL") {
Ok(None)
} else {
p.parse::<i64>()
.map(Some)
.map_err(|_| EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type bigint: {p:?}"),
})
}
})
.collect()
}
fn decode_text_array_external(s: &str) -> Result<Vec<Option<String>>, EvalError> {
let trimmed = s.trim();
let inner = trimmed
.strip_prefix('{')
.and_then(|x| x.strip_suffix('}'))
.or_else(|| trimmed.strip_prefix('[').and_then(|x| x.strip_suffix(']')))
.ok_or_else(|| EvalError::TypeMismatch {
detail: alloc::format!("TEXT[] literal {s:?} must be enclosed in '{{...}}'"),
})?;
let mut out: Vec<Option<String>> = Vec::new();
if inner.trim().is_empty() {
return Ok(out);
}
let bytes = inner.as_bytes();
let mut i = 0;
while i <= bytes.len() {
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
if i < bytes.len() && bytes[i] == b'"' {
i += 1;
let mut buf = String::new();
while i < bytes.len() && bytes[i] != b'"' {
if bytes[i] == b'\\' && i + 1 < bytes.len() {
buf.push(bytes[i + 1] as char);
i += 2;
} else {
buf.push(bytes[i] as char);
i += 1;
}
}
if i >= bytes.len() {
return Err(EvalError::TypeMismatch {
detail: "unterminated quoted element in TEXT[] literal".into(),
});
}
i += 1;
out.push(Some(buf));
} else {
let start = i;
while i < bytes.len() && bytes[i] != b',' {
i += 1;
}
let raw = inner[start..i].trim();
if raw.eq_ignore_ascii_case("NULL") {
out.push(None);
} else {
out.push(Some(raw.to_string()));
}
}
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
if i >= bytes.len() {
break;
}
if bytes[i] != b',' {
return Err(EvalError::TypeMismatch {
detail: "expected ',' between TEXT[] elements".into(),
});
}
i += 1;
}
Ok(out)
}
fn cast_to_interval(v: Value) -> Result<Value, EvalError> {
match v {
Value::Interval {
months,
days,
micros,
} => Ok(Value::Interval {
months,
days,
micros,
}),
Value::Text(s) => {
let (months, days, micros) =
spg_sql::parser::parse_interval_text(&s).ok_or_else(|| {
EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type interval: \"{s}\""),
}
})?;
Ok(Value::Interval {
months,
days,
micros,
})
}
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::INTERVAL only accepts TEXT-shape inputs, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
fn cast_to_date(v: Value) -> Result<Value, EvalError> {
match v {
Value::Date(d) => Ok(Value::Date(d)),
Value::Int(n) => Ok(Value::Date(n)),
Value::BigInt(n) => {
i32::try_from(n)
.map(Value::Date)
.map_err(|_| EvalError::TypeMismatch {
detail: "bigint days-since-epoch out of DATE range".into(),
})
}
Value::Timestamp(t) => {
let days = t.div_euclid(86_400_000_000);
i32::try_from(days)
.map(Value::Date)
.map_err(|_| EvalError::TypeMismatch {
detail: "timestamp out of DATE range".into(),
})
}
Value::Text(s) => {
if let Some(d) = parse_date_literal(&s) {
return Ok(Value::Date(d));
}
if let Some(t) = parse_timestamp_literal(&s) {
let days = t.div_euclid(86_400_000_000);
return i32::try_from(days)
.map(Value::Date)
.map_err(|_| EvalError::TypeMismatch {
detail: "timestamp out of DATE range".into(),
});
}
if super::format::date_text_is_field_shaped(&s) {
return Err(EvalError::TypeMismatch {
detail: format!(
"date/time field value out of range: {s:?}\n\
HINT: Perhaps you need a different \"DateStyle\" setting."
),
});
}
Err(EvalError::TypeMismatch {
detail: format!("invalid input syntax for type date: {s:?}"),
})
}
other => Err(EvalError::TypeMismatch {
detail: format!(
"cannot cast {} to DATE",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
fn cast_to_timestamp(v: Value) -> Result<Value, EvalError> {
match v {
Value::Timestamp(t) => Ok(Value::Timestamp(t)),
Value::Int(n) => Ok(Value::Timestamp(i64::from(n))),
Value::BigInt(n) => Ok(Value::Timestamp(n)),
Value::Date(d) => Ok(Value::Timestamp(crate::conversions::date_days_to_micros(d))),
Value::Text(s) => {
crate::eval::format::parse_timestamp_literal_wall_ordered(
&s,
crate::eval::format::DateOrder::Mdy,
)
.map(Value::Timestamp)
.ok_or_else(|| EvalError::TypeMismatch {
detail: crate::eval::format::datetime_input_error_text(&s, "timestamp"),
})
}
other => Err(EvalError::TypeMismatch {
detail: format!(
"cannot cast {} to TIMESTAMP",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
fn cast_to_timestamptz(v: Value) -> Result<Value, EvalError> {
let Value::Text(s) = &v else {
return cast_to_timestamp(v);
};
crate::eval::format::parse_timestamp_literal_tz_ordered(s, crate::eval::format::DateOrder::Mdy)
.map(|(micros, _had_tz)| Value::Timestamp(micros))
.ok_or_else(|| EvalError::TypeMismatch {
detail: crate::eval::format::datetime_input_error_text(s, "timestamp with time zone"),
})
}
fn cast_numeric_special_reject(
v: &Value,
target: &str,
) -> Option<Result<Value<'static>, EvalError>> {
let Value::Numeric { kind, .. } = v else {
return None;
};
if *kind == spg_storage::NumericKind::Finite {
return None;
}
let what = if *kind == spg_storage::NumericKind::NaN {
"NaN"
} else {
"infinity"
};
Some(Err(EvalError::TypeMismatch {
detail: alloc::format!("cannot convert {what} to {target}"),
}))
}
fn cast_numeric_to_int(v: Value) -> Result<Value, EvalError> {
match v {
Value::SmallInt(n) => Ok(Value::Int(i32::from(n))),
Value::Int(n) => Ok(Value::Int(n)),
Value::BigInt(n) => i32::try_from(n)
.map(Value::Int)
.map_err(|_| EvalError::TypeMismatch {
detail: "integer out of range".into(),
}),
#[allow(clippy::cast_possible_truncation)]
Value::Float(x) => {
let r = f64_round_half_even(x);
if !r.is_finite() || !(-2_147_483_648.0..=2_147_483_647.0).contains(&r) {
return Err(EvalError::TypeMismatch {
detail: "integer out of range".into(),
});
}
Ok(Value::Int(r as i32))
}
#[allow(clippy::cast_possible_truncation)]
Value::Real(x) => {
let r = f64_round_half_even(f64::from(x));
if !r.is_finite() || !(-2_147_483_648.0..=2_147_483_647.0).contains(&r) {
return Err(EvalError::TypeMismatch {
detail: "integer out of range".into(),
});
}
Ok(Value::Int(r as i32))
}
Value::Numeric { scaled, scale, .. } => {
let rounded = numeric_round_to_i128(scaled, scale);
i32::try_from(rounded)
.map(Value::Int)
.map_err(|_| EvalError::TypeMismatch {
detail: "integer out of range".into(),
})
}
Value::Text(s) => crate::conversions::parse_pg_int(&s)
.and_then(|n| i32::try_from(n).ok())
.map(Value::Int)
.ok_or_else(|| EvalError::TypeMismatch {
detail: format!("invalid input syntax for type integer: {s:?}"),
}),
Value::Bool(b) => Ok(Value::Int(i32::from(b))),
Value::Char1(b) => Ok(Value::Int(i32::from(b))),
#[allow(clippy::cast_possible_truncation)]
Value::BitString { nbits, bytes } => Ok(Value::Int(crate::conversions::bit_string_to_i64(
nbits, &bytes,
) as i32)),
Value::Json(s) => match crate::conversions::jsonb_scalar_for_cast(&s, "integer")? {
crate::conversions::JsonbScalar::Numeric(n) => cast_numeric_to_int(n),
crate::conversions::JsonbScalar::Bool(_) => Err(
crate::conversions::jsonb_cast_type_error("boolean", "integer"),
),
crate::conversions::JsonbScalar::Null => Ok(Value::Null),
},
other => Err(EvalError::TypeMismatch {
detail: format!(
"cannot cast {} to int",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
fn cast_numeric_to_bigint(v: Value) -> Result<Value, EvalError> {
match v {
Value::Int(n) => Ok(Value::BigInt(i64::from(n))),
Value::SmallInt(n) => Ok(Value::BigInt(i64::from(n))),
Value::BigInt(n) => Ok(Value::BigInt(n)),
#[allow(clippy::cast_possible_truncation)]
Value::Float(x) => {
let r = f64_round_half_even(x);
if !r.is_finite()
|| !(-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&r)
{
return Err(EvalError::TypeMismatch {
detail: "bigint out of range".into(),
});
}
Ok(Value::BigInt(r as i64))
}
#[allow(clippy::cast_possible_truncation)]
Value::Real(x) => {
let r = f64_round_half_even(f64::from(x));
if !r.is_finite()
|| !(-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&r)
{
return Err(EvalError::TypeMismatch {
detail: "bigint out of range".into(),
});
}
Ok(Value::BigInt(r as i64))
}
Value::Numeric { scaled, scale, .. } => {
let rounded = numeric_round_to_i128(scaled, scale);
i64::try_from(rounded)
.map(Value::BigInt)
.map_err(|_| EvalError::TypeMismatch {
detail: format!("numeric {rounded} does not fit in bigint"),
})
}
Value::Text(s) => crate::conversions::parse_pg_int(&s)
.map(Value::BigInt)
.ok_or_else(|| EvalError::TypeMismatch {
detail: format!("invalid input syntax for type bigint: \"{s}\""),
}),
Value::Bool(b) => Ok(Value::BigInt(i64::from(b))),
Value::BitString { nbits, bytes } => Ok(Value::BigInt(
crate::conversions::bit_string_to_i64(nbits, &bytes),
)),
Value::Json(s) => match crate::conversions::jsonb_scalar_for_cast(&s, "bigint")? {
crate::conversions::JsonbScalar::Numeric(n) => cast_numeric_to_bigint(n),
crate::conversions::JsonbScalar::Bool(_) => Err(
crate::conversions::jsonb_cast_type_error("boolean", "bigint"),
),
crate::conversions::JsonbScalar::Null => Ok(Value::Null),
},
other => Err(EvalError::TypeMismatch {
detail: format!(
"cannot cast {} to bigint",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
fn cast_numeric_to_float(v: Value) -> Result<Value, EvalError> {
match v {
Value::Int(n) => Ok(Value::Float(f64::from(n))),
#[allow(clippy::cast_precision_loss)]
Value::BigInt(n) => Ok(Value::Float(n as f64)),
Value::Float(x) => Ok(Value::Float(x)),
#[allow(clippy::cast_precision_loss)]
Value::Numeric { kind, .. } if kind != spg_storage::NumericKind::Finite => {
Ok(Value::Float(match kind {
spg_storage::NumericKind::NaN => f64::NAN,
spg_storage::NumericKind::PosInf => f64::INFINITY,
_ => f64::NEG_INFINITY,
}))
}
Value::Numeric { scaled, scale, .. } => Ok(Value::Float(
(scaled as f64) / f64_powi(10.0, i32::from(scale)),
)),
Value::Text(s) => {
let t = s.trim();
if t.parse::<f64>().is_err() {
return Err(EvalError::TypeMismatch {
detail: format!("cannot parse {s:?} as float"),
});
}
crate::conversions::parse_float8(t)
.map(Value::Float)
.ok_or_else(|| EvalError::TypeMismatch {
detail: format!("\"{t}\" is out of range for type double precision"),
})
}
Value::Json(s) => {
match crate::conversions::jsonb_scalar_for_cast(&s, "double precision")? {
crate::conversions::JsonbScalar::Numeric(n) => cast_numeric_to_float(n),
crate::conversions::JsonbScalar::Bool(_) => Err(
crate::conversions::jsonb_cast_type_error("boolean", "double precision"),
),
crate::conversions::JsonbScalar::Null => Ok(Value::Null),
}
}
other => Err(EvalError::TypeMismatch {
detail: format!(
"cannot cast {} to float",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
fn cast_to_bool(v: Value) -> Result<Value, EvalError> {
match v {
Value::Bool(b) => Ok(Value::Bool(b)),
Value::Int(n) => Ok(Value::Bool(n != 0)),
Value::BigInt(n) => Ok(Value::Bool(n != 0)),
Value::Text(s) => {
let lo = s.trim().to_ascii_lowercase();
match lo.as_str() {
"1" | "t" | "tr" | "tru" | "true" | "y" | "ye" | "yes" | "on" => {
Ok(Value::Bool(true))
}
"0" | "f" | "fa" | "fal" | "fals" | "false" | "n" | "no" | "of" | "off" => {
Ok(Value::Bool(false))
}
_ => Err(EvalError::TypeMismatch {
detail: format!("invalid input syntax for type boolean: {:?}", s.trim()),
}),
}
}
Value::Json(s) => match crate::conversions::jsonb_scalar_for_cast(&s, "boolean")? {
crate::conversions::JsonbScalar::Bool(b) => Ok(Value::Bool(b)),
crate::conversions::JsonbScalar::Numeric(_) => Err(
crate::conversions::jsonb_cast_type_error("numeric", "boolean"),
),
crate::conversions::JsonbScalar::Null => Ok(Value::Null),
},
other => Err(EvalError::TypeMismatch {
detail: format!(
"cannot cast {} to bool",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
pub fn cast_to_vector(v: Value) -> Result<Value<'static>, EvalError> {
match v {
Value::Null => Ok(Value::Null),
Value::Vector(v) => Ok(Value::vector(v.into_owned())),
Value::Text(s) => {
parse_vector_text(&s)
.map(Value::vector)
.ok_or_else(|| EvalError::TypeMismatch {
detail: format!("cannot parse {s:?} as a vector literal"),
})
}
other => Err(EvalError::TypeMismatch {
detail: format!(
"::vector requires text input, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
pub fn parse_vector_text(s: &str) -> Option<Vec<f32>> {
let trimmed = s.trim();
let inner = trimmed.strip_prefix('[')?.strip_suffix(']')?;
let trimmed_inner = inner.trim();
if trimmed_inner.is_empty() {
return Some(Vec::new());
}
let mut out = Vec::new();
for part in trimmed_inner.split(',') {
let f: f32 = part.trim().parse().ok()?;
out.push(f);
}
Some(out)
}
#[cfg(test)]
mod round613_plain_named_targets {
use super::*;
fn assert_no_arm_above_the_resolve_claims(name: &str) {
assert!(
!REG_MISC_TYPES.iter().any(|k| name.eq_ignore_ascii_case(k)),
"{name} is a reg-misc type"
);
assert!(
!CATALOG_SCALAR_TYPES
.iter()
.any(|k| name.eq_ignore_ascii_case(k)),
"{name} is a catalog scalar"
);
assert!(
!OPAQUE_TYPES.iter().any(|k| name.eq_ignore_ascii_case(k)),
"{name} is a pseudotype"
);
for special in [
"__bit_literal",
"tid",
"xid",
"xid8",
"jsonpath",
"binary",
"signed",
"unsigned",
] {
assert!(
!name.eq_ignore_ascii_case(special),
"{name} has its own arm ({special})"
);
}
assert!(bit_cast_width(name).is_none(), "{name} is a bit spelling");
assert!(
temporal_typmod(name).is_none(),
"{name} carries a temporal precision"
);
assert!(
!is_bare_temporal_type(name),
"{name} is a bare temporal type"
);
assert!(
matches!(cast_catalog_scalar(name, &Value::text("x")), Ok(None)),
"{name} is claimed by the catalog-scalar arm"
);
}
#[test]
fn every_plain_target_resolves_to_the_type_the_table_claims() {
for (name, dt) in PLAIN_NAMED_TARGETS {
assert_eq!(
crate::conversions::type_name_to_data_type(name),
Some(*dt),
"{name} does not resolve to the type the table gives it"
);
assert_eq!(plain_named_target(name), Some(*dt));
assert_eq!(plain_named_target(&name.to_uppercase()), Some(*dt));
assert_no_arm_above_the_resolve_claims(name);
}
}
#[test]
fn every_typmod_head_is_plain_and_resolves_through_the_type_table() {
for head in PLAIN_NAMED_HEADS {
assert_no_arm_above_the_resolve_claims(head);
for spelled in [alloc::format!("{head}(4)"), alloc::format!("{head}(10,2)")] {
assert_no_arm_above_the_resolve_claims(&spelled);
assert_eq!(
plain_named_target(&spelled),
crate::conversions::type_name_to_data_type(&spelled),
"{spelled} takes a different type through the shortcut"
);
}
let bare = plain_named_target(head);
let exact = PLAIN_NAMED_TARGETS
.iter()
.find(|(k, _)| head.eq_ignore_ascii_case(k))
.map(|(_, dt)| *dt);
assert_eq!(bare, exact, "{head} bare");
}
}
#[test]
fn a_name_with_its_own_arm_is_not_shortcut() {
for name in [
"regproc",
"aclitem",
"anyarray",
"tid",
"xid",
"jsonpath",
"bit",
"bit(4)",
"timestamp",
"timestamp(2)",
"time(3)",
"nosuchtype",
"int4range",
] {
assert_eq!(plain_named_target(name), None, "{name} was shortcut");
}
}
}