use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use spg_sql::ast::CastTarget;
use spg_storage::Value;
use super::{
EvalError, decode_tsquery_external, decode_tsvector_external, parse_date_literal,
parse_timestamp_literal, value_to_text,
};
pub fn cast_value(v: Value, target: CastTarget) -> Result<Value, EvalError> {
if matches!(v, Value::Null) {
return Ok(Value::Null);
}
match target {
CastTarget::Vector => cast_to_vector(v),
CastTarget::Text => Ok(Value::Text(value_to_text(&v))),
CastTarget::Int => cast_numeric_to_int(v),
CastTarget::BigInt => 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 | CastTarget::Timestamptz => cast_to_timestamp(v),
CastTarget::Interval => cast_to_interval(v),
CastTarget::Json | CastTarget::Jsonb => match v {
Value::Json(s) => Ok(Value::Json(s)),
Value::Text(s) => Ok(Value::Json(s)),
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::json / ::jsonb only accepts TEXT-shape inputs, got {:?}",
other.data_type()
),
}),
},
CastTarget::RegType | CastTarget::RegClass => match v {
Value::Text(s) => {
let bare = s.rsplit('.').next().unwrap_or(&s).to_string();
Ok(Value::Text(bare))
}
Value::Int(n) => Ok(Value::Text(alloc::format!("{n}"))),
Value::BigInt(n) => Ok(Value::Text(alloc::format!("{n}"))),
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::regtype / ::regclass accepts TEXT (name) or integer (oid), got {:?}",
other.data_type()
),
}),
},
CastTarget::TextArray => match v {
Value::TextArray(items) => Ok(Value::TextArray(items)),
Value::Text(s) => decode_text_array_external(&s).map(Value::TextArray),
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::TEXT[] only accepts TEXT / TEXT[] inputs, got {:?}",
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 {:?}",
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 {:?}",
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 {:?}",
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}"),
}),
},
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::bytea only accepts TEXT / bytea inputs, got {:?}",
other.data_type()
),
}),
},
}
}
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) => 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 {:?}", 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) => 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 {:?}", other.data_type()),
}),
}
}
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('}'))
.ok_or_else(|| EvalError::TypeMismatch {
detail: alloc::format!("INT[] literal {s:?} must be enclosed in '{{...}}'"),
})?;
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!("INT[] element {p:?} is not an i32"),
})
}
})
.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('}'))
.ok_or_else(|| EvalError::TypeMismatch {
detail: alloc::format!("BIGINT[] literal {s:?} must be enclosed in '{{...}}'"),
})?;
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!("BIGINT[] element {p:?} is not an i64"),
})
}
})
.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('}'))
.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, micros } => Ok(Value::Interval { months, micros }),
Value::Text(s) => {
let (months, micros) = spg_sql::parser::parse_interval_text(&s).ok_or_else(|| {
EvalError::TypeMismatch {
detail: alloc::format!("cannot parse {s:?} as INTERVAL"),
}
})?;
Ok(Value::Interval { months, micros })
}
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"::INTERVAL only accepts TEXT-shape inputs, got {:?}",
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) => parse_date_literal(&s)
.map(Value::Date)
.ok_or(EvalError::TypeMismatch {
detail: format!("cannot parse {s:?} as DATE (expected YYYY-MM-DD)"),
}),
other => Err(EvalError::TypeMismatch {
detail: format!("cannot cast {:?} to DATE", 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(i64::from(d) * 86_400_000_000)),
Value::Text(s) => {
parse_timestamp_literal(&s)
.map(Value::Timestamp)
.ok_or(EvalError::TypeMismatch {
detail: format!(
"cannot parse {s:?} as TIMESTAMP \
(expected YYYY-MM-DD[ HH:MM:SS[.ffffff]])"
),
})
}
other => Err(EvalError::TypeMismatch {
detail: format!("cannot cast {:?} to TIMESTAMP", other.data_type()),
}),
}
}
fn cast_numeric_to_int(v: Value) -> Result<Value, EvalError> {
match v {
Value::Int(n) => Ok(Value::Int(n)),
Value::BigInt(n) => i32::try_from(n)
.map(Value::Int)
.map_err(|_| EvalError::TypeMismatch {
detail: format!("bigint {n} does not fit in int"),
}),
#[allow(clippy::cast_possible_truncation)]
Value::Float(x) => Ok(Value::Int(x as i32)),
Value::Text(s) => {
s.trim()
.parse::<i32>()
.map(Value::Int)
.map_err(|_| EvalError::TypeMismatch {
detail: format!("cannot parse {s:?} as int"),
})
}
Value::Bool(b) => Ok(Value::Int(i32::from(b))),
other => Err(EvalError::TypeMismatch {
detail: format!("cannot cast {:?} to int", 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::BigInt(n) => Ok(Value::BigInt(n)),
#[allow(clippy::cast_possible_truncation)]
Value::Float(x) => Ok(Value::BigInt(x as i64)),
Value::Text(s) => {
s.trim()
.parse::<i64>()
.map(Value::BigInt)
.map_err(|_| EvalError::TypeMismatch {
detail: format!("cannot parse {s:?} as bigint"),
})
}
Value::Bool(b) => Ok(Value::BigInt(i64::from(b))),
other => Err(EvalError::TypeMismatch {
detail: format!("cannot cast {:?} to bigint", 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)),
Value::Text(s) => {
s.trim()
.parse::<f64>()
.map(Value::Float)
.map_err(|_| EvalError::TypeMismatch {
detail: format!("cannot parse {s:?} as float"),
})
}
other => Err(EvalError::TypeMismatch {
detail: format!("cannot cast {:?} to float", 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() {
"true" | "t" | "yes" | "y" | "1" | "on" => Ok(Value::Bool(true)),
"false" | "f" | "no" | "n" | "0" | "off" => Ok(Value::Bool(false)),
_ => Err(EvalError::TypeMismatch {
detail: format!("cannot parse {s:?} as bool"),
}),
}
}
other => Err(EvalError::TypeMismatch {
detail: format!("cannot cast {:?} to bool", other.data_type()),
}),
}
}
pub fn cast_to_vector(v: Value) -> Result<Value, EvalError> {
match v {
Value::Null => Ok(Value::Null),
Value::Vector(v) => Ok(Value::Vector(v)),
Value::Text(s) => parse_vector_text(&s)
.map(Value::Vector)
.ok_or(EvalError::TypeMismatch {
detail: format!("cannot parse {s:?} as a vector literal"),
}),
other => Err(EvalError::TypeMismatch {
detail: format!("::vector requires text input, got {:?}", 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)
}