use alloc::string::ToString;
use alloc::vec::Vec;
use spg_sql::ast::{ColumnTypeName, Expr, Literal, UnOp, VecEncoding as SqlVecEncoding};
use spg_storage::{ColumnSchema, DataType, StorageError, Value, VecEncoding};
use crate::EngineError;
use crate::eval::{self, EvalContext, EvalError};
use crate::numeric::{
numeric_from_float, numeric_from_integer, numeric_rescale, numeric_round_to_integer,
parse_numeric_text,
};
pub(crate) fn decode_bytea_literal(s: &str) -> Result<alloc::vec::Vec<u8>, alloc::string::String> {
let s = s.trim();
if let Some(hex) = s.strip_prefix("\\x").or_else(|| s.strip_prefix("\\X")) {
let cleaned: alloc::string::String = hex.chars().filter(|c| !c.is_whitespace()).collect();
if cleaned.len() % 2 != 0 {
return Err(alloc::string::String::from(
"invalid hexadecimal data: odd number of digits",
));
}
let mut out = alloc::vec::Vec::with_capacity(cleaned.len() / 2);
let cleaned_bytes = cleaned.as_bytes();
for i in (0..cleaned_bytes.len()).step_by(2) {
let hi = hex_nibble(cleaned_bytes[i]).map_err(|()| bad_hex_digit(cleaned_bytes[i]))?;
let lo = hex_nibble(cleaned_bytes[i + 1])
.map_err(|()| bad_hex_digit(cleaned_bytes[i + 1]))?;
out.push((hi << 4) | lo);
}
return Ok(out);
}
let bytes = s.as_bytes();
let mut out = alloc::vec::Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b'\\' && i + 1 < bytes.len() {
let n = bytes[i + 1];
if n == b'\\' {
out.push(b'\\');
i += 2;
continue;
}
if n.is_ascii_digit()
&& i + 3 < bytes.len()
&& bytes[i + 2].is_ascii_digit()
&& bytes[i + 3].is_ascii_digit()
{
let oct = |x: u8| (x - b'0') as u32;
let v = oct(n) * 64 + oct(bytes[i + 2]) * 8 + oct(bytes[i + 3]);
if v <= 0xFF {
out.push(v as u8);
i += 4;
continue;
}
}
}
out.push(b);
i += 1;
}
Ok(out)
}
pub(crate) fn hex_nibble(b: u8) -> Result<u8, ()> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'a'..=b'f' => Ok(b - b'a' + 10),
b'A'..=b'F' => Ok(b - b'A' + 10),
_ => Err(()),
}
}
fn bad_hex_digit(b: u8) -> alloc::string::String {
alloc::format!("invalid hexadecimal digit: \"{}\"", b as char)
}
#[derive(Clone, Copy)]
enum UniformArrayKind {
Bool,
Float,
Numeric,
Date,
Timestamp,
Uuid,
Bytes,
Interval,
Money,
}
impl UniformArrayKind {
fn build(self, items: alloc::vec::Vec<Value<'static>>) -> Value<'static> {
match self {
Self::Bool => Value::BoolArray(
items
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Bool(b) => Some(b),
_ => unreachable!("uniform Bool"),
})
.collect(),
),
Self::Float => Value::FloatArray(
items
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Float(x) => Some(x),
_ => unreachable!("uniform Float"),
})
.collect(),
),
Self::Numeric => Value::NumericArray(
items
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Numeric { scaled, scale, .. } => Some((scaled, scale)),
_ => unreachable!("uniform Numeric"),
})
.collect(),
),
Self::Date => Value::DateArray(
items
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Date(d) => Some(d),
_ => unreachable!("uniform Date"),
})
.collect(),
),
Self::Timestamp => Value::TimestampArray(
items
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Timestamp(t) => Some(t),
_ => unreachable!("uniform Timestamp"),
})
.collect(),
),
Self::Uuid => Value::UuidArray(
items
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Uuid(b) => Some(b),
_ => unreachable!("uniform Uuid"),
})
.collect(),
),
Self::Bytes => Value::BytesArray(
items
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Bytes(b) => Some(b.into_owned()),
_ => unreachable!("uniform Bytes"),
})
.collect(),
),
Self::Interval => Value::IntervalArray(
items
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Interval {
months,
days,
micros,
} => Some(spg_storage::IntervalSpan {
months,
days,
micros,
}),
_ => unreachable!("uniform Interval"),
})
.collect(),
),
Self::Money => Value::MoneyArray(
items
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Money(c) => Some(c),
_ => unreachable!("uniform Money"),
})
.collect(),
),
}
}
}
fn widen_uniform_typed(items: &[Value<'static>]) -> Option<UniformArrayKind> {
let mut kind: Option<UniformArrayKind> = None;
let mut saw_non_null = false;
for v in items {
let this = match v {
Value::Null => continue,
Value::Bool(_) => UniformArrayKind::Bool,
Value::Float(_) => UniformArrayKind::Float,
Value::Numeric { .. } => UniformArrayKind::Numeric,
Value::Date(_) => UniformArrayKind::Date,
Value::Timestamp(_) => UniformArrayKind::Timestamp,
Value::Uuid(_) => UniformArrayKind::Uuid,
Value::Bytes(_) => UniformArrayKind::Bytes,
Value::Interval { .. } => UniformArrayKind::Interval,
Value::Money(_) => UniformArrayKind::Money,
_ => return None,
};
match kind {
None => kind = Some(this),
Some(prev) if discriminant_eq(prev, this) => {}
Some(_) => return None,
}
saw_non_null = true;
}
if saw_non_null { kind } else { None }
}
fn discriminant_eq(a: UniformArrayKind, b: UniformArrayKind) -> bool {
matches!(
(a, b),
(UniformArrayKind::Bool, UniformArrayKind::Bool)
| (UniformArrayKind::Float, UniformArrayKind::Float)
| (UniformArrayKind::Numeric, UniformArrayKind::Numeric)
| (UniformArrayKind::Date, UniformArrayKind::Date)
| (UniformArrayKind::Timestamp, UniformArrayKind::Timestamp)
| (UniformArrayKind::Uuid, UniformArrayKind::Uuid)
| (UniformArrayKind::Bytes, UniformArrayKind::Bytes)
| (UniformArrayKind::Interval, UniformArrayKind::Interval)
| (UniformArrayKind::Money, UniformArrayKind::Money)
)
}
pub(crate) fn array_literal_widen(items: alloc::vec::Vec<Value<'static>>) -> Value<'static> {
if let Some(m) = crate::eval::values::build_2d_from_rows(&items) {
return m;
}
if let Some(arr) = widen_uniform_typed(&items) {
return arr.build(items);
}
let mut has_text = false;
let mut has_bigint = false;
let mut has_int = false;
for v in &items {
match v {
Value::Null => {}
Value::Text(_) | Value::Json(_) => has_text = true,
Value::BigInt(_) => has_bigint = true,
Value::Int(_) | Value::SmallInt(_) => has_int = true,
_ => has_text = true,
}
}
if has_text || (!has_bigint && !has_int) {
let out: alloc::vec::Vec<Option<alloc::string::String>> = items
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Text(s) | Value::Json(s) => Some(s.into_owned()),
other => Some(alloc::format!("{other:?}")),
})
.collect();
return Value::TextArray(out);
}
if has_bigint {
let out: alloc::vec::Vec<Option<i64>> = items
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Int(n) => Some(i64::from(n)),
Value::SmallInt(n) => Some(i64::from(n)),
Value::BigInt(n) => Some(n),
_ => unreachable!("widen: unexpected non-integer in BigInt path"),
})
.collect();
return Value::BigIntArray(out);
}
let out: alloc::vec::Vec<Option<i32>> = items
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Int(n) => Some(n),
Value::SmallInt(n) => Some(i32::from(n)),
_ => unreachable!("widen: unexpected non-i32-compatible in Int path"),
})
.collect();
Value::IntArray(out)
}
#[must_use]
pub(crate) fn malformed_array_literal(text: &str) -> alloc::string::String {
let t = text.trim();
let detail = if !t.starts_with('{') {
"Array value must start with \"{\" or dimension information."
} else {
match first_unquoted_close_brace(&t[1..]) {
None => "Unexpected end of input.",
Some(close) => {
let inner = &t[1..1 + close];
if !t[1 + close + 1..].trim().is_empty() {
"Junk after closing right brace."
} else if inner.trim_end().ends_with(',') {
"Unexpected \"}\" character."
} else {
"Unexpected end of input."
}
}
}
};
alloc::format!("malformed array literal: \"{text}\" DETAIL: {detail}")
}
fn first_unquoted_close_brace(body: &str) -> Option<usize> {
let bs = body.as_bytes();
let mut in_quote = false;
let mut k = 0;
while k < bs.len() {
match bs[k] {
b'\\' if in_quote => k += 1,
b'"' => in_quote = !in_quote,
b'}' if !in_quote => return Some(k),
_ => {}
}
k += 1;
}
None
}
pub(crate) fn decode_text_array_literal(
s: &str,
) -> Result<alloc::vec::Vec<Option<alloc::string::String>>, &'static str> {
let trimmed = s.trim();
let body = trimmed
.strip_prefix('{')
.ok_or("TEXT[] literal must be enclosed in '{...}'")?;
let close =
first_unquoted_close_brace(body).ok_or("TEXT[] literal must be enclosed in '{...}'")?;
if !body[close + 1..].trim().is_empty() {
return Err("junk after closing right brace");
}
let inner = &body[..close];
let mut out: alloc::vec::Vec<Option<alloc::string::String>> = alloc::vec::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 = alloc::string::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("unterminated quoted element");
}
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.is_empty() {
return Err("empty array element");
}
if raw.eq_ignore_ascii_case("NULL") {
out.push(None);
} else {
out.push(Some(alloc::string::ToString::to_string(raw)));
}
}
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
if i >= bytes.len() {
break;
}
if bytes[i] != b',' {
return Err("expected ',' between TEXT[] elements");
}
i += 1;
}
Ok(out)
}
pub(crate) fn encode_text_array(items: &[Option<alloc::string::String>]) -> alloc::string::String {
let mut out = alloc::string::String::with_capacity(2 + items.len() * 8);
out.push('{');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
match item {
None => out.push_str("NULL"),
Some(s) => {
let needs_quote = s.is_empty()
|| s.eq_ignore_ascii_case("NULL")
|| s.chars()
.any(|c| matches!(c, ',' | '{' | '}' | '"' | '\\' | ' ' | '\t'));
if needs_quote {
out.push('"');
for c in s.chars() {
if c == '"' || c == '\\' {
out.push('\\');
}
out.push(c);
}
out.push('"');
} else {
out.push_str(s);
}
}
}
}
out.push('}');
out
}
pub(crate) fn encode_bytea_hex(b: &[u8]) -> alloc::string::String {
let mut out = alloc::string::String::with_capacity(2 + 2 * b.len());
out.push_str("\\x");
for byte in b {
let hi = byte >> 4;
let lo = byte & 0x0F;
out.push(hex_digit(hi));
out.push(hex_digit(lo));
}
out
}
pub(crate) const fn hex_digit(n: u8) -> char {
match n {
0..=9 => (b'0' + n) as char,
10..=15 => (b'a' + n - 10) as char,
_ => '?',
}
}
pub(crate) fn parse_hstore_str(
s: &str,
) -> Option<Vec<(alloc::string::String, Option<alloc::string::String>)>> {
let bytes = s.as_bytes();
let mut i = 0;
let mut out: Vec<(alloc::string::String, Option<alloc::string::String>)> = Vec::new();
let skip_ws = |bytes: &[u8], i: &mut usize| {
while *i < bytes.len() && matches!(bytes[*i], b' ' | b'\t' | b'\n' | b'\r') {
*i += 1;
}
};
let parse_token = |bytes: &[u8], i: &mut usize| -> Option<alloc::string::String> {
if *i >= bytes.len() {
return None;
}
if bytes[*i] == b'"' {
*i += 1;
let mut out = alloc::string::String::new();
while *i < bytes.len() {
match bytes[*i] {
b'"' => {
*i += 1;
return Some(out);
}
b'\\' if *i + 1 < bytes.len() => {
out.push(bytes[*i + 1] as char);
*i += 2;
}
c => {
out.push(c as char);
*i += 1;
}
}
}
None
} else {
let start = *i;
while *i < bytes.len()
&& !matches!(bytes[*i], b' ' | b'\t' | b'\n' | b'\r' | b',' | b'=')
{
*i += 1;
}
if *i == start {
return None;
}
Some(alloc::str::from_utf8(&bytes[start..*i]).ok()?.to_string())
}
};
skip_ws(bytes, &mut i);
while i < bytes.len() {
let key = parse_token(bytes, &mut i)?;
skip_ws(bytes, &mut i);
if i + 1 >= bytes.len() || bytes[i] != b'=' || bytes[i + 1] != b'>' {
return None;
}
i += 2;
skip_ws(bytes, &mut i);
let val_token = if i + 4 <= bytes.len()
&& bytes[i..i + 4].eq_ignore_ascii_case(b"NULL")
&& (i + 4 == bytes.len() || matches!(bytes[i + 4], b' ' | b'\t' | b',' | b'\n' | b'\r'))
{
i += 4;
None
} else {
Some(parse_token(bytes, &mut i)?)
};
if out.iter().any(|(k, _)| k == &key) {
} else {
out.push((key, val_token));
}
skip_ws(bytes, &mut i);
if i >= bytes.len() {
break;
}
if bytes[i] == b',' {
i += 1;
skip_ws(bytes, &mut i);
continue;
}
return None;
}
Some(out)
}
pub(crate) fn format_hstore_str(
pairs: &[(alloc::string::String, Option<alloc::string::String>)],
) -> alloc::string::String {
let mut out = alloc::string::String::new();
for (i, (k, v)) in pairs.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push('"');
out.push_str(k);
out.push_str("\"=>");
match v {
None => out.push_str("NULL"),
Some(val) => {
out.push('"');
out.push_str(val);
out.push('"');
}
}
}
out
}
pub fn format_hstore_text(
pairs: &[(alloc::string::String, Option<alloc::string::String>)],
) -> alloc::string::String {
format_hstore_str(pairs)
}
pub(crate) fn split_2d_literal(s: &str) -> Result<Vec<Vec<alloc::string::String>>, &'static str> {
let s = s.trim();
let outer = s
.strip_prefix('{')
.and_then(|x| x.strip_suffix('}'))
.ok_or("missing outer '{...}' braces")?;
let trimmed = outer.trim();
if trimmed.is_empty() {
return Ok(Vec::new());
}
let mut rows: Vec<Vec<alloc::string::String>> = Vec::new();
let mut i = 0;
let bytes = trimmed.as_bytes();
while i < bytes.len() {
while i < bytes.len() && matches!(bytes[i], b' ' | b'\t' | b'\n' | b'\r' | b',') {
i += 1;
}
if i >= bytes.len() {
break;
}
if bytes[i] != b'{' {
return Err("expected '{' opening a row");
}
i += 1;
let row_start = i;
let mut depth = 1;
while i < bytes.len() && depth > 0 {
match bytes[i] {
b'{' => depth += 1,
b'}' => depth -= 1,
_ => {}
}
if depth > 0 {
i += 1;
}
}
if depth != 0 {
return Err("unbalanced '{...}' in row");
}
let row_text = &trimmed[row_start..i];
i += 1;
let cells: Vec<alloc::string::String> = if row_text.trim().is_empty() {
Vec::new()
} else {
row_text.split(',').map(|t| t.trim().to_string()).collect()
};
rows.push(cells);
}
if let Some(first) = rows.first() {
let cols = first.len();
for r in &rows {
if r.len() != cols {
return Err("ragged 2D array (rows have different column counts)");
}
}
}
Ok(rows)
}
pub(crate) fn parse_int_2d_literal(s: &str) -> Result<Vec<Vec<Option<i32>>>, &'static str> {
let raw = split_2d_literal(s)?;
raw.into_iter()
.map(|row| {
row.into_iter()
.map(|cell| {
if cell.eq_ignore_ascii_case("NULL") {
Ok(None)
} else {
cell.parse::<i32>()
.map(Some)
.map_err(|_| "invalid int element")
}
})
.collect()
})
.collect()
}
pub(crate) fn parse_bigint_2d_literal(s: &str) -> Result<Vec<Vec<Option<i64>>>, &'static str> {
let raw = split_2d_literal(s)?;
raw.into_iter()
.map(|row| {
row.into_iter()
.map(|cell| {
if cell.eq_ignore_ascii_case("NULL") {
Ok(None)
} else {
cell.parse::<i64>()
.map(Some)
.map_err(|_| "invalid bigint element")
}
})
.collect()
})
.collect()
}
pub(crate) fn parse_text_2d_literal(
s: &str,
) -> Result<Vec<Vec<Option<alloc::string::String>>>, &'static str> {
let raw = split_2d_literal(s)?;
Ok(raw
.into_iter()
.map(|row| {
row.into_iter()
.map(|cell| {
if cell.eq_ignore_ascii_case("NULL") {
None
} else {
Some(cell.trim_matches('"').to_string())
}
})
.collect()
})
.collect())
}
pub(crate) fn format_int_2d_text(rows: &[Vec<Option<i32>>]) -> alloc::string::String {
let mut out = alloc::string::String::from("{");
for (i, row) in rows.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push('{');
for (j, cell) in row.iter().enumerate() {
if j > 0 {
out.push(',');
}
match cell {
None => out.push_str("NULL"),
Some(n) => out.push_str(&alloc::format!("{n}")),
}
}
out.push('}');
}
out.push('}');
out
}
pub(crate) fn format_bigint_2d_text(rows: &[Vec<Option<i64>>]) -> alloc::string::String {
let mut out = alloc::string::String::from("{");
for (i, row) in rows.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push('{');
for (j, cell) in row.iter().enumerate() {
if j > 0 {
out.push(',');
}
match cell {
None => out.push_str("NULL"),
Some(n) => out.push_str(&alloc::format!("{n}")),
}
}
out.push('}');
}
out.push('}');
out
}
pub(crate) fn format_text_2d_text(
rows: &[Vec<Option<alloc::string::String>>],
) -> alloc::string::String {
let mut out = alloc::string::String::from("{");
for (i, row) in rows.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push('{');
for (j, cell) in row.iter().enumerate() {
if j > 0 {
out.push(',');
}
match cell {
None => out.push_str("NULL"),
Some(s) => out.push_str(s),
}
}
out.push('}');
}
out.push('}');
out
}
pub fn format_int_2d_text_pub(rows: &[Vec<Option<i32>>]) -> alloc::string::String {
format_int_2d_text(rows)
}
pub fn format_bigint_2d_text_pub(rows: &[Vec<Option<i64>>]) -> alloc::string::String {
format_bigint_2d_text(rows)
}
pub fn format_text_2d_text_pub(
rows: &[Vec<Option<alloc::string::String>>],
) -> alloc::string::String {
format_text_2d_text(rows)
}
#[must_use]
pub fn format_bool_2d_text_pub(rows: &[Vec<Option<bool>>]) -> alloc::string::String {
use core::fmt::Write as _;
let mut out = alloc::string::String::from("{");
for (i, row) in rows.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push('{');
for (j, cell) in row.iter().enumerate() {
if j > 0 {
out.push(',');
}
let _ = match cell {
None => write!(out, "NULL"),
Some(true) => write!(out, "t"),
Some(false) => write!(out, "f"),
};
}
out.push('}');
}
out.push('}');
out
}
pub(crate) type CanonRangeBounds = (
Option<Value<'static>>,
Option<Value<'static>>,
bool,
bool,
bool,
);
pub(crate) fn canonicalize_range_bounds(
kind: spg_storage::RangeKind,
lower: Option<Value<'static>>,
upper: Option<Value<'static>>,
lower_inc: bool,
upper_inc: bool,
) -> Option<CanonRangeBounds> {
use spg_storage::RangeKind as K;
let mut lower_inc = lower.is_some() && lower_inc;
let mut upper_inc = upper.is_some() && upper_inc;
let mut lower = lower;
let mut upper = upper;
if matches!(kind, K::Int4 | K::Int8 | K::Date) {
fn succ(v: Value<'static>) -> Option<Value<'static>> {
Some(match v {
Value::Int(n) => Value::Int(n.checked_add(1)?),
Value::BigInt(n) => Value::BigInt(n.checked_add(1)?),
Value::Date(d) => Value::Date(d.checked_add(1)?),
other => other,
})
}
if let Some(l) = lower {
lower = Some(if lower_inc { l } else { succ(l)? });
lower_inc = true;
}
if let Some(u) = upper {
upper = Some(if upper_inc { succ(u)? } else { u });
upper_inc = false;
}
}
let empty = match (&lower, &upper) {
(Some(l), Some(u)) => l == u && !(lower_inc && upper_inc),
_ => false,
};
Some((lower, upper, lower_inc, upper_inc, empty))
}
pub(crate) enum RangeParseError {
Malformed,
Misordered,
BadElement(alloc::string::String),
}
fn range_element_type_name(kind: spg_storage::RangeKind) -> &'static str {
match kind {
spg_storage::RangeKind::Int4 => "integer",
spg_storage::RangeKind::Int8 => "bigint",
spg_storage::RangeKind::Num => "numeric",
spg_storage::RangeKind::Ts => "timestamp",
spg_storage::RangeKind::TsTz => "timestamp with time zone",
spg_storage::RangeKind::Date => "date",
}
}
pub(crate) fn range_bounds_misordered(
lower: &Option<Value<'static>>,
upper: &Option<Value<'static>>,
) -> bool {
match (lower, upper) {
(Some(l), Some(u)) => crate::orderby::value_cmp(l, u) == core::cmp::Ordering::Greater,
_ => false,
}
}
pub(crate) fn parse_range_str(
s: &str,
kind: spg_storage::RangeKind,
) -> Result<Value<'static>, RangeParseError> {
let s = s.trim();
if s.eq_ignore_ascii_case("empty") {
return Ok(Value::Range {
kind,
lower: None,
upper: None,
lower_inc: false,
upper_inc: false,
empty: true,
});
}
let bytes = s.as_bytes();
if bytes.len() < 3 {
return Err(RangeParseError::Malformed);
}
let lower_inc = match bytes[0] {
b'[' => true,
b'(' => false,
_ => return Err(RangeParseError::Malformed),
};
let upper_inc = match bytes[bytes.len() - 1] {
b']' => true,
b')' => false,
_ => return Err(RangeParseError::Malformed),
};
let inner = &s[1..s.len() - 1];
let (lo_text, up_text) = inner.split_once(',').ok_or(RangeParseError::Malformed)?;
let lower = if lo_text.is_empty() {
None
} else {
Some(
parse_range_element(lo_text, kind)
.ok_or_else(|| RangeParseError::BadElement(lo_text.trim().into()))?,
)
};
let upper = if up_text.is_empty() {
None
} else {
Some(
parse_range_element(up_text, kind)
.ok_or_else(|| RangeParseError::BadElement(up_text.trim().into()))?,
)
};
if range_bounds_misordered(&lower, &upper) {
return Err(RangeParseError::Misordered);
}
let (lower, upper, lower_inc, upper_inc, empty) =
canonicalize_range_bounds(kind, lower, upper, lower_inc, upper_inc)
.ok_or(RangeParseError::Malformed)?;
Ok(Value::Range {
kind,
lower: lower.map(alloc::boxed::Box::new),
upper: upper.map(alloc::boxed::Box::new),
lower_inc,
upper_inc,
empty,
})
}
pub(crate) fn parse_multirange_str(
s: &str,
kind: spg_storage::RangeKind,
) -> Option<Vec<spg_storage::RangeSpan>> {
let s = s.trim();
let inner = s.strip_prefix('{').and_then(|x| x.strip_suffix('}'))?;
let inner = inner.trim();
if inner.is_empty() {
return Some(Vec::new());
}
let mut spans: Vec<spg_storage::RangeSpan> = Vec::new();
let bytes = inner.as_bytes();
let mut depth: i32 = 0;
let mut start = 0usize;
for i in 0..=bytes.len() {
let cut = i == bytes.len() || (depth == 0 && bytes[i] == b',');
if !cut {
match bytes.get(i) {
Some(b'[') | Some(b'(') => depth += 1,
Some(b']') | Some(b')') => depth -= 1,
_ => {}
}
continue;
}
let piece = inner[start..i].trim();
if piece.is_empty() {
return None;
}
let r = parse_range_str(piece, kind).ok()?;
let Value::Range {
lower,
upper,
lower_inc,
upper_inc,
empty,
..
} = r
else {
return None;
};
spans.push(spg_storage::RangeSpan {
lower,
upper,
lower_inc,
upper_inc,
empty,
});
start = i + 1;
}
Some(spans)
}
fn parse_hhmm_offset_secs(off: &str) -> Option<i32> {
let (h, m) = match off.split_once(':') {
Some((h, m)) => (h, m),
None => (off, "0"),
};
let h: i32 = h.parse().ok()?;
let m: i32 = m.parse().ok()?;
if !(0..=15).contains(&h) || !(0..60).contains(&m) {
return None;
}
Some(h * 3600 + m * 60)
}
pub(crate) fn regtype_name_to_oid(name: &str) -> Option<i64> {
if let Some(base) = name.trim().strip_suffix("[]") {
return array_oid_for_element(regtype_name_to_oid(base)?);
}
Some(match name.trim() {
"bool" | "boolean" => 16,
"bytea" => 17,
"name" => 19,
"int8" | "bigint" => 20,
"int2" | "smallint" => 21,
"int4" | "int" | "integer" => 23,
"text" => 25,
"oid" => 26,
"json" => 114,
"xml" => 142,
"float4" | "real" => 700,
"float8" | "double precision" => 701,
"cidr" => 650,
"inet" => 869,
"macaddr" => 829,
"macaddr8" => 774,
"money" => 790,
"bpchar" | "char" | "character" => 1042,
"varchar" | "character varying" => 1043,
"date" => 1082,
"time" | "time without time zone" => 1083,
"timestamp" | "timestamp without time zone" => 1114,
"timestamptz" | "timestamp with time zone" => 1184,
"interval" => 1186,
"timetz" | "time with time zone" => 1266,
"numeric" | "decimal" => 1700,
"uuid" => 2950,
"jsonb" => 3802,
"tsvector" => 3614,
"tsquery" => 3615,
"pg_lsn" => 3220,
"regtype" => 2206,
"regclass" => 2205,
"regproc" => 24,
"xid" => 28,
"xid8" => 5069,
"tid" => 27,
"cid" => 29,
_ => return None,
})
}
pub(crate) fn regtype_canonical_name(name: &str) -> Option<alloc::string::String> {
let t = name.trim();
if let Some(base) = t.strip_suffix("[]") {
let inner = regtype_canonical_name(base)?;
return Some(alloc::format!("{inner}[]"));
}
if let Some(base) = t.strip_prefix('_') {
let inner = regtype_canonical_name(base)?;
return Some(alloc::format!("{inner}[]"));
}
let oid = regtype_name_to_oid(&t.to_lowercase())?;
regtype_oid_to_name(oid).map(alloc::string::String::from)
}
pub(crate) fn parse_range_element(
text: &str,
kind: spg_storage::RangeKind,
) -> Option<Value<'static>> {
let text = text.trim().trim_matches('"');
use spg_storage::RangeKind as K;
match kind {
K::Int4 => text.parse::<i32>().ok().map(Value::Int),
K::Int8 => text.parse::<i64>().ok().map(Value::BigInt),
K::Num => {
let dot = text.find('.');
let scale: u16 = dot.map_or(0, |p| (text.len() - p - 1) as u16);
let digits: alloc::string::String = text
.chars()
.filter(|c| *c == '-' || c.is_ascii_digit())
.collect();
let scaled: i128 = digits.parse().ok()?;
Some(Value::Numeric {
scaled,
scale,
kind: spg_storage::NumericKind::Finite,
})
}
K::Ts | K::TsTz => {
crate::eval::parse_timestamp_literal(text)
.or_else(|| {
let (date_part, off) = text.split_once(['+'])?;
if !off.chars().all(|c| c.is_ascii_digit() || c == ':') {
return None;
}
let d = crate::eval::parse_date_literal(date_part.trim())?;
let mut t = i64::from(d) * 86_400_000_000;
let secs = parse_hhmm_offset_secs(off)?;
t -= i64::from(secs) * 1_000_000;
Some(t)
})
.map(Value::Timestamp)
}
K::Date => crate::eval::parse_date_literal(text).map(Value::Date),
}
}
pub fn format_range_text(v: &Value) -> alloc::string::String {
format_range_str(v)
}
pub(crate) fn format_range_str(v: &Value) -> alloc::string::String {
let Value::Range {
kind,
lower,
upper,
lower_inc,
upper_inc,
empty,
} = v
else {
return alloc::string::String::new();
};
if *empty {
return "empty".into();
}
let elem = |v: &Value| -> alloc::string::String {
let base = format_range_element(v);
if matches!(kind, spg_storage::RangeKind::TsTz) && matches!(v, Value::Timestamp(_)) {
alloc::format!("{base}+00")
} else {
base
}
};
let mut out = alloc::string::String::new();
out.push(if *lower_inc { '[' } else { '(' });
if let Some(l) = lower {
out.push_str("e_range_bound(&elem(l)));
}
out.push(',');
if let Some(u) = upper {
out.push_str("e_range_bound(&elem(u)));
}
out.push(if *upper_inc { ']' } else { ')' });
out
}
fn quote_range_bound(s: &str) -> alloc::string::String {
let needs_quote = s.is_empty()
|| s.chars()
.any(|c| matches!(c, '"' | '\\' | '(' | ')' | '[' | ']' | ',') || c.is_whitespace());
if !needs_quote {
return s.into();
}
let mut out = alloc::string::String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
if c == '"' || c == '\\' {
out.push('\\');
}
out.push(c);
}
out.push('"');
out
}
pub fn format_point(p: spg_storage::Point2D) -> alloc::string::String {
alloc::format!("({},{})", p.x, p.y)
}
pub fn format_lseg(p1: spg_storage::Point2D, p2: spg_storage::Point2D) -> alloc::string::String {
alloc::format!("[({},{}),({},{})]", p1.x, p1.y, p2.x, p2.y)
}
pub fn format_pg_box(ur: spg_storage::Point2D, ll: spg_storage::Point2D) -> alloc::string::String {
alloc::format!("({},{}),({},{})", ur.x, ur.y, ll.x, ll.y)
}
pub fn format_line(a: f64, b: f64, c: f64) -> alloc::string::String {
alloc::format!("{{{},{},{}}}", a, b, c)
}
pub fn format_circle(center: spg_storage::Point2D, radius: f64) -> alloc::string::String {
alloc::format!("<({},{}),{}>", center.x, center.y, radius)
}
pub fn format_path(points: &[spg_storage::Point2D], closed: bool) -> alloc::string::String {
let (open, close) = if closed { ('(', ')') } else { ('[', ']') };
let mut out = alloc::string::String::new();
out.push(open);
for (i, p) in points.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&alloc::format!("({},{})", p.x, p.y));
}
out.push(close);
out
}
pub fn format_polygon(points: &[spg_storage::Point2D]) -> alloc::string::String {
let mut out = alloc::string::String::new();
out.push('(');
for (i, p) in points.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&alloc::format!("({},{})", p.x, p.y));
}
out.push(')');
out
}
fn parse_point(s: &str) -> Option<spg_storage::Point2D> {
let s = s.trim();
let inner = s
.strip_prefix('(')
.and_then(|x| x.strip_suffix(')'))
.unwrap_or(s);
let (xs, ys) = inner.split_once(',')?;
let x: f64 = xs.trim().parse().ok()?;
let y: f64 = ys.trim().parse().ok()?;
Some(spg_storage::Point2D { x, y })
}
fn parse_point_list(s: &str) -> Option<Vec<spg_storage::Point2D>> {
let bytes = s.as_bytes();
let mut out: Vec<spg_storage::Point2D> = Vec::new();
let mut depth: i32 = 0;
let mut start = 0usize;
for i in 0..=bytes.len() {
let cut = i == bytes.len() || (depth == 0 && bytes[i] == b',');
if !cut {
match bytes.get(i) {
Some(b'(') | Some(b'[') | Some(b'<') => depth += 1,
Some(b')') | Some(b']') | Some(b'>') => depth -= 1,
_ => {}
}
continue;
}
let piece = s[start..i].trim();
if !piece.is_empty() {
out.push(parse_point(piece)?);
}
start = i + 1;
}
Some(out)
}
pub fn parse_lseg_text(s: &str) -> Option<(spg_storage::Point2D, spg_storage::Point2D)> {
let s = s.trim();
let inner = s
.strip_prefix('[')
.and_then(|x| x.strip_suffix(']'))
.unwrap_or(s);
let two_points = |v: Option<alloc::vec::Vec<spg_storage::Point2D>>| v.filter(|p| p.len() == 2);
let pts = if let Some(p) = two_points(parse_point_list(inner)) {
p
} else {
inner
.strip_prefix('(')
.and_then(|x| x.strip_suffix(')'))
.and_then(|w| two_points(parse_point_list(w)))?
};
Some((pts[0], pts[1]))
}
pub fn parse_box_text(s: &str) -> Option<(spg_storage::Point2D, spg_storage::Point2D)> {
let s = s.trim();
let two_points = |v: Option<alloc::vec::Vec<spg_storage::Point2D>>| v.filter(|p| p.len() == 2);
let pts = if let Some(p) = two_points(parse_point_list(s)) {
p
} else if let Some(p) = s
.strip_prefix('(')
.and_then(|x| x.strip_suffix(')'))
.and_then(|inner| two_points(parse_point_list(inner)))
{
p
} else {
let nums: Option<alloc::vec::Vec<f64>> =
s.split(',').map(|t| t.trim().parse::<f64>().ok()).collect();
let nums = nums?;
if nums.len() != 4 {
return None;
}
alloc::vec![
spg_storage::Point2D {
x: nums[0],
y: nums[1]
},
spg_storage::Point2D {
x: nums[2],
y: nums[3]
},
]
};
if pts.len() != 2 {
return None;
}
let (a, b) = (pts[0], pts[1]);
let ur = spg_storage::Point2D {
x: a.x.max(b.x),
y: a.y.max(b.y),
};
let ll = spg_storage::Point2D {
x: a.x.min(b.x),
y: a.y.min(b.y),
};
Some((ur, ll))
}
pub fn parse_line_text(s: &str) -> Option<(f64, f64, f64)> {
let s = s.trim();
if let Some(inner) = s.strip_prefix('{').and_then(|x| x.strip_suffix('}')) {
let parts: Vec<&str> = inner.split(',').collect();
if parts.len() != 3 {
return None;
}
let a: f64 = parts[0].trim().parse().ok()?;
let b: f64 = parts[1].trim().parse().ok()?;
if a == 0.0 && b == 0.0 {
return None;
}
let c: f64 = parts[2].trim().parse().ok()?;
return Some((a, b, c));
}
let (p1, p2) = parse_lseg_text(s)?;
if p1.x == p2.x && p1.y == p2.y {
return None;
}
Some(line_from_points(p1, p2))
}
pub fn line_from_points(p1: spg_storage::Point2D, p2: spg_storage::Point2D) -> (f64, f64, f64) {
if p1.x == p2.x {
(-1.0, 0.0, p1.x)
} else if p1.y == p2.y {
(0.0, -1.0, p1.y)
} else {
let m = (p1.y - p2.y) / (p1.x - p2.x);
let c = p1.y - m * p1.x;
(m, -1.0, if c == 0.0 { 0.0 } else { c })
}
}
pub fn parse_circle_text(s: &str) -> Option<(spg_storage::Point2D, f64)> {
let s = s.trim();
let inner = if let Some(i) = s.strip_prefix('<').and_then(|x| x.strip_suffix('>')) {
i
} else if let Some(i) = s.strip_prefix('(').and_then(|x| x.strip_suffix(')')) {
i
} else {
s
};
let bytes = inner.as_bytes();
let mut depth = 0i32;
let mut split_at: Option<usize> = None;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'(' | b'[' | b'<' => depth += 1,
b')' | b']' | b'>' => depth -= 1,
b',' if depth == 0 => split_at = Some(i),
_ => {}
}
}
let i = split_at?;
let center = parse_point(&inner[..i])?;
let radius: f64 = inner[i + 1..].trim().parse().ok()?;
Some((center, radius))
}
pub fn parse_path_text(s: &str) -> Option<(Vec<spg_storage::Point2D>, bool)> {
let s = s.trim();
if let Some(i) = s.strip_prefix('[').and_then(|x| x.strip_suffix(']')) {
if let Some(pts) = parse_point_list(i) {
return Some((pts, false));
}
}
if let Some(i) = s.strip_prefix('(').and_then(|x| x.strip_suffix(')')) {
if let Some(pts) = parse_point_list(i) {
return Some((pts, true));
}
}
parse_point_list(s).map(|pts| (pts, true))
}
pub fn parse_polygon_text(s: &str) -> Option<Vec<spg_storage::Point2D>> {
let s = s.trim();
if let Some(inner) = s.strip_prefix('(').and_then(|x| x.strip_suffix(')')) {
if let Some(pts) = parse_point_list(inner) {
return Some(pts);
}
}
parse_point_list(s)
}
pub fn format_inet_full(family: u8, bits: u8, addr: &[u8; 16]) -> alloc::string::String {
let max = if family == 4 { 32 } else { 128 };
let base = format_inet(family, max, addr);
alloc::format!("{base}/{bits}")
}
pub fn format_inet(family: u8, bits: u8, addr: &[u8; 16]) -> alloc::string::String {
match family {
4 => {
let s = alloc::format!("{}.{}.{}.{}", addr[0], addr[1], addr[2], addr[3]);
if bits == 32 {
s
} else {
alloc::format!("{s}/{bits}")
}
}
6 => {
let mut groups = [0u16; 8];
for (i, g) in groups.iter_mut().enumerate() {
*g = (u16::from(addr[i * 2]) << 8) | u16::from(addr[i * 2 + 1]);
}
if groups[..5].iter().all(|&g| g == 0) && groups[5] == 0xffff {
let s =
alloc::format!("::ffff:{}.{}.{}.{}", addr[12], addr[13], addr[14], addr[15]);
return if bits == 128 {
s
} else {
alloc::format!("{s}/{bits}")
};
}
let (mut best_start, mut best_len) = (usize::MAX, 0usize);
let mut i = 0;
while i < 8 {
if groups[i] == 0 {
let start = i;
while i < 8 && groups[i] == 0 {
i += 1;
}
if i - start > best_len {
best_start = start;
best_len = i - start;
}
} else {
i += 1;
}
}
let mut out = alloc::string::String::new();
if best_len >= 2 {
for (idx, g) in groups.iter().enumerate().take(best_start) {
if idx > 0 {
out.push(':');
}
out.push_str(&alloc::format!("{g:x}"));
}
out.push_str("::");
for (idx, g) in groups.iter().enumerate().skip(best_start + best_len) {
if idx > best_start + best_len {
out.push(':');
}
out.push_str(&alloc::format!("{g:x}"));
}
} else {
for (idx, g) in groups.iter().enumerate() {
if idx > 0 {
out.push(':');
}
out.push_str(&alloc::format!("{g:x}"));
}
}
if bits == 128 {
out
} else {
alloc::format!("{out}/{bits}")
}
}
_ => alloc::format!("?invalid-inet-family-{family}"),
}
}
pub fn format_macaddr(m: &[u8; 6]) -> alloc::string::String {
alloc::format!(
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
m[0],
m[1],
m[2],
m[3],
m[4],
m[5]
)
}
pub fn format_macaddr8(m: &[u8; 8]) -> alloc::string::String {
alloc::format!(
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
m[0],
m[1],
m[2],
m[3],
m[4],
m[5],
m[6],
m[7]
)
}
pub fn format_bit_string(nbits: u32, bytes: &[u8]) -> alloc::string::String {
let mut out = alloc::string::String::with_capacity(nbits as usize);
for i in 0..nbits as usize {
let byte = bytes[i / 8];
let bit = (byte >> (7 - (i % 8))) & 1;
out.push(if bit == 1 { '1' } else { '0' });
}
out
}
pub fn bit_string_to_i64(nbits: u32, bytes: &[u8]) -> i64 {
let mut val: i64 = 0;
for i in 0..nbits as usize {
let byte = bytes.get(i / 8).copied().unwrap_or(0);
val = (val << 1) | i64::from((byte >> (7 - (i % 8))) & 1);
}
val
}
pub fn format_money_array(items: &[Option<i64>]) -> alloc::string::String {
let mut out = alloc::string::String::new();
out.push('{');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
match item {
None => out.push_str("NULL"),
Some(c) => out.push_str(&crate::eval::format_money(*c)),
}
}
out.push('}');
out
}
pub fn parse_inet_text(s: &str) -> Option<(u8, u8, [u8; 16])> {
let s = s.trim();
let (addr_s, bits_s) = match s.split_once('/') {
Some((a, b)) => (a, Some(b)),
None => (s, None),
};
if addr_s.contains(':') {
let (head, tail) = match addr_s.find("::") {
Some(idx) => (&addr_s[..idx], Some(&addr_s[idx + 2..])),
None => (addr_s, None),
};
let mut head_groups: alloc::vec::Vec<&str> = if head.is_empty() {
alloc::vec::Vec::new()
} else {
head.split(':').collect()
};
let mut tail_groups: alloc::vec::Vec<&str> = match tail {
Some(t) if !t.is_empty() => t.split(':').collect(),
_ => alloc::vec::Vec::new(),
};
let mut dotted_words: Option<[u16; 2]> = None;
if let Some(g) = tail_groups.last().or_else(|| head_groups.last()) {
if g.contains('.') {
let oct: alloc::vec::Vec<&str> = g.split('.').collect();
if oct.len() != 4 {
return None;
}
let mut b = [0u8; 4];
for (i, o) in oct.iter().enumerate() {
b[i] = o.parse::<u8>().ok()?;
}
dotted_words = Some([
(u16::from(b[0]) << 8) | u16::from(b[1]),
(u16::from(b[2]) << 8) | u16::from(b[3]),
]);
if !tail_groups.is_empty() {
tail_groups.pop();
} else {
head_groups.pop();
}
}
}
let dq = if dotted_words.is_some() { 2 } else { 0 };
let head_len = head_groups.len();
let tail_len = tail_groups.len();
if tail.is_none() {
if head_len + dq != 8 {
return None;
}
} else if head_len + tail_len + dq > 7 {
return None;
}
let mut words = [0u16; 8];
for (i, g) in head_groups.iter().enumerate() {
words[i] = u16::from_str_radix(g, 16).ok()?;
}
let trailing_start = 8 - dq - tail_len;
for (i, g) in tail_groups.iter().enumerate() {
words[trailing_start + i] = u16::from_str_radix(g, 16).ok()?;
}
if let Some(dw) = dotted_words {
words[6] = dw[0];
words[7] = dw[1];
}
let mut addr = [0u8; 16];
for (i, w) in words.iter().enumerate() {
addr[i * 2] = (w >> 8) as u8;
addr[i * 2 + 1] = (w & 0xff) as u8;
}
let bits = match bits_s {
Some(b) => b.parse::<u8>().ok().filter(|&n| n <= 128)?,
None => 128,
};
Some((6, bits, addr))
} else {
let parts: alloc::vec::Vec<&str> = addr_s.split('.').collect();
if parts.len() != 4 {
return None;
}
let mut addr = [0u8; 16];
for (i, p) in parts.iter().enumerate() {
addr[i] = p.parse::<u8>().ok()?;
}
let bits = match bits_s {
Some(b) => b.parse::<u8>().ok().filter(|&n| n <= 32)?,
None => 32,
};
Some((4, bits, addr))
}
}
pub fn parse_cidr_text(s: &str) -> Result<Option<(u8, u8, [u8; 16])>, ()> {
let s = s.trim();
let parsed = if !s.contains(':') {
let (addr_s, bits_s) = match s.split_once('/') {
Some((a, b)) => (a, Some(b)),
None => (s, None),
};
let parts: alloc::vec::Vec<&str> = addr_s.split('.').collect();
if parts.is_empty() || parts.len() > 4 || parts.iter().any(|p| p.is_empty()) {
return Ok(None);
}
let mut addr = [0u8; 16];
for (i, p) in parts.iter().enumerate() {
match p.parse::<u8>() {
Ok(v) => addr[i] = v,
Err(_) => return Ok(None),
}
}
let bits = match bits_s {
Some(b) => match b.parse::<u8>() {
Ok(n) if n <= 32 => n,
_ => return Ok(None),
},
None => (parts.len() as u8) * 8,
};
Some((4u8, bits, addr))
} else {
parse_inet_text(s).map(|(f, b, a)| {
(f, if s.contains('/') { b } else { 128 }, a)
})
};
let Some((family, bits, addr)) = parsed else {
return Ok(None);
};
let total = if family == 4 { 32u16 } else { 128 };
let nbytes = if family == 4 { 4 } else { 16 };
for byte in 0..nbytes {
let bit_base = (byte as u16) * 8;
let keep = (u16::from(bits)).saturating_sub(bit_base).min(8) as u8;
let mask: u8 = if keep == 0 { 0 } else { 0xffu8 << (8 - keep) };
if addr[byte] & !mask != 0 {
return Err(());
}
if bit_base >= total {
break;
}
}
Ok(Some((family, bits, addr)))
}
pub fn parse_macaddr_text(s: &str) -> Option<[u8; 6]> {
let s = s.trim();
let cleaned: alloc::string::String = s.chars().filter(|c| c.is_ascii_hexdigit()).collect();
if cleaned.len() != 12 {
return None;
}
let mut out = [0u8; 6];
for i in 0..6 {
out[i] = u8::from_str_radix(&cleaned[i * 2..i * 2 + 2], 16).ok()?;
}
Some(out)
}
#[must_use]
pub fn date_days_to_micros(d: i32) -> i64 {
match d {
i32::MAX => i64::MAX,
i32::MIN => i64::MIN,
_ => i64::from(d) * 86_400_000_000,
}
}
pub fn parse_pg_lsn_text(s: &str) -> Option<u64> {
let t = s.trim();
let (hi, lo) = t.split_once('/')?;
if hi.is_empty() || lo.is_empty() || hi.len() > 8 || lo.len() > 8 {
return None;
}
let hi = u32::from_str_radix(hi, 16).ok()?;
let lo = u32::from_str_radix(lo, 16).ok()?;
Some((u64::from(hi) << 32) | u64::from(lo))
}
#[must_use]
pub fn format_pg_lsn(l: u64) -> alloc::string::String {
alloc::format!("{:X}/{:X}", l >> 32, l & 0xFFFF_FFFF)
}
pub fn parse_macaddr8_text(s: &str) -> Option<[u8; 8]> {
let s = s.trim();
let cleaned: alloc::string::String = s.chars().filter(|c| c.is_ascii_hexdigit()).collect();
if cleaned.len() == 12 {
let mut six = [0u8; 6];
for i in 0..6 {
six[i] = u8::from_str_radix(&cleaned[i * 2..i * 2 + 2], 16).ok()?;
}
return Some([six[0], six[1], six[2], 0xff, 0xfe, six[3], six[4], six[5]]);
}
if cleaned.len() != 16 {
return None;
}
let mut out = [0u8; 8];
for i in 0..8 {
out[i] = u8::from_str_radix(&cleaned[i * 2..i * 2 + 2], 16).ok()?;
}
Some(out)
}
pub fn parse_bit_string_text(s: &str) -> Option<(u32, alloc::vec::Vec<u8>)> {
let s = s.trim();
let nbits = u32::try_from(s.len()).ok()?;
let nbytes = (s.len()).div_ceil(8);
let mut bytes = alloc::vec![0u8; nbytes];
for (i, c) in s.chars().enumerate() {
let bit = match c {
'0' => 0u8,
'1' => 1u8,
_ => return None,
};
if bit == 1 {
bytes[i / 8] |= 1 << (7 - (i % 8));
}
}
Some((nbits, bytes))
}
pub fn format_multirange(ranges: &[spg_storage::RangeSpan]) -> alloc::string::String {
let mut out = alloc::string::String::new();
out.push('{');
for (i, r) in ranges.iter().enumerate() {
if i > 0 {
out.push(',');
}
if r.empty {
out.push_str("empty");
continue;
}
out.push(if r.lower_inc { '[' } else { '(' });
if let Some(l) = &r.lower {
out.push_str("e_range_bound(&format_range_element(l)));
}
out.push(',');
if let Some(u) = &r.upper {
out.push_str("e_range_bound(&format_range_element(u)));
}
out.push(if r.upper_inc { ']' } else { ')' });
}
out.push('}');
out
}
pub(crate) fn format_range_element(v: &Value) -> alloc::string::String {
match v {
Value::Int(n) => alloc::format!("{n}"),
Value::BigInt(n) => alloc::format!("{n}"),
Value::Date(d) => crate::eval::format_date(*d),
Value::Timestamp(t) => crate::eval::format_timestamp(*t),
Value::Numeric {
scaled,
scale,
kind,
} => crate::eval::format_numeric_kind(*kind, *scaled, *scale),
other => alloc::format!("{other:?}"),
}
}
pub(crate) fn parse_money_str(s: &str) -> Option<i64> {
let mut rest = s.trim();
let mut neg = false;
loop {
let before = rest;
rest = rest.trim_start();
if let Some(r) = rest.strip_prefix('$') {
rest = r;
} else if let Some(r) = rest.strip_prefix('-') {
neg = true;
rest = r;
} else if let Some(r) = rest.strip_prefix('(') {
neg = true;
rest = r;
} else if let Some(r) = rest.strip_prefix('+') {
rest = r;
}
if rest == before {
break;
}
}
let (int_part, tail) = {
let end = rest
.find(|c: char| !(c.is_ascii_digit() || c == ','))
.unwrap_or(rest.len());
(&rest[..end], &rest[end..])
};
let mut int_digits = alloc::string::String::with_capacity(int_part.len());
for b in int_part.bytes() {
match b {
b',' => {}
b'0'..=b'9' => int_digits.push(b as char),
_ => return None,
}
}
if int_digits.is_empty() {
return None;
}
let dollars: i64 = int_digits.parse().ok()?;
let (mut cents, tail) = match tail.strip_prefix('.') {
None => (0i64, tail),
Some(f) => {
let end = f.find(|c: char| !c.is_ascii_digit()).unwrap_or(f.len());
let (digits, rest_tail) = (&f[..end], &f[end..]);
if digits.is_empty() {
return None;
}
let b = digits.as_bytes();
let mut c = i64::from(b[0] - b'0') * 10;
if b.len() >= 2 {
c += i64::from(b[1] - b'0');
}
if b.len() >= 3 && b[2] >= b'5' {
c += 1;
}
(c, rest_tail)
}
};
let mut tail = tail;
while !tail.is_empty() {
let t = tail.trim_start();
if let Some(r) = t.strip_prefix(')') {
tail = r;
} else if let Some(r) = t.strip_prefix('-') {
neg = true;
tail = r;
} else if let Some(r) = t.strip_prefix('+') {
tail = r;
} else if let Some(r) = t.strip_prefix('$') {
tail = r;
} else if t.is_empty() {
break;
} else {
return None;
}
}
let carry = cents / 100;
cents %= 100;
let total = dollars
.checked_add(carry)?
.checked_mul(100)?
.checked_add(cents)?;
Some(if neg { -total } else { total })
}
pub(crate) fn parse_timetz_str(s: &str) -> Option<(i64, i32)> {
let s = s.trim();
let bytes = s.as_bytes();
let sign_pos = bytes
.iter()
.enumerate()
.rev()
.find(|&(_, &b)| b == b'+' || b == b'-')
.map(|(i, _)| i)?;
if sign_pos == 0 {
return None; }
let time_part = &s[..sign_pos];
let offset_part = &s[sign_pos..];
let us = parse_time_str(time_part)?;
let sign: i32 = if offset_part.starts_with('+') { 1 } else { -1 };
let offset_body = &offset_part[1..];
let (hh_str, mm_str) = match offset_body.split_once(':') {
Some((h, m)) => (h, m),
None if offset_body.len() == 4 => offset_body.split_at(2),
None if offset_body.len() == 3 => offset_body.split_at(1),
None => (offset_body, "0"),
};
let hh: i32 = hh_str.parse().ok()?;
let mm: i32 = mm_str.parse().ok()?;
if !(0..=14).contains(&hh) || !(0..=59).contains(&mm) {
return None;
}
let total = sign * (hh * 3600 + mm * 60);
if total.abs() > 50_400 {
return None;
}
Some((us, total))
}
pub(crate) fn coerce_int_to_year(n: i64, col_name: &str) -> Result<Value<'static>, EngineError> {
if n == 0 || (1901..=2155).contains(&n) {
return Ok(Value::Year(n as u16));
}
Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!(
"year value out of range: {n} (column `{col_name}`; \
MySQL accepts 0 or 1901..=2155)"
),
}))
}
pub(crate) fn parse_time_str(s: &str) -> Option<i64> {
let s = s.trim();
if s.eq_ignore_ascii_case("allballs") {
return Some(0);
}
let (hms, frac) = match s.split_once('.') {
Some((h, f)) => (h, Some(f)),
None => (s, None),
};
let mut parts = hms.split(':');
let hh: u32 = parts.next()?.parse().ok()?;
let mm: u32 = parts.next()?.parse().ok()?;
let ss: u32 = match parts.next() {
Some(x) => x.parse().ok()?,
None => 0,
};
if parts.next().is_some() {
return None;
}
if hh > 24 || mm > 59 || ss > 59 || (hh == 24 && (mm != 0 || ss != 0)) {
return None;
}
let frac_us: i64 = match frac {
None => 0,
Some(f) => {
if f.is_empty() || f.len() > 6 || !f.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let mut padded = alloc::string::String::with_capacity(6);
padded.push_str(f);
while padded.len() < 6 {
padded.push('0');
}
padded.parse().ok()?
}
};
if hh == 24 && frac_us != 0 {
return None;
}
Some(
i64::from(hh) * 3_600_000_000
+ i64::from(mm) * 60_000_000
+ i64::from(ss) * 1_000_000
+ frac_us,
)
}
pub(crate) fn numeric_typmod_in_range(precision: u16, scale: i16) -> bool {
(1..=1000).contains(&precision) && (-1000..=1000).contains(&scale)
}
pub(crate) fn numeric_typmod_error(name: &str) -> Option<alloc::string::String> {
let lower = name.trim().to_ascii_lowercase();
let (head, rest) = lower.split_once('(')?;
if !matches!(head.trim(), "numeric" | "decimal") {
return None;
}
let args = rest.strip_suffix(')')?;
let mut it = args.split(',').map(str::trim);
let p: i64 = it.next()?.parse().ok()?;
if !(1..=1000).contains(&p) {
return Some(alloc::format!(
"NUMERIC precision {p} must be between 1 and 1000"
));
}
if let Some(s) = it.next() {
let s: i64 = s.parse().ok()?;
if !(-1000..=1000).contains(&s) {
return Some(alloc::format!(
"NUMERIC scale {s} must be between -1000 and 1000"
));
}
}
None
}
pub(crate) fn type_name_to_data_type(name: &str) -> Option<DataType> {
with_lower_name(name.trim(), type_name_to_data_type_lower)
}
pub(crate) fn with_lower_name<R>(name: &str, f: impl FnOnce(&str) -> R) -> R {
const CAP: usize = 64;
if name.len() <= CAP {
let mut buf = [0u8; CAP];
buf[..name.len()].copy_from_slice(name.as_bytes());
buf[..name.len()].make_ascii_lowercase();
if let Ok(s) = core::str::from_utf8(&buf[..name.len()]) {
return f(s);
}
}
f(&name.to_ascii_lowercase())
}
fn type_name_to_data_type_lower(n: &str) -> Option<DataType> {
if let Some((head, paren)) = n.split_once('(')
&& let Some(args) = paren.strip_suffix(')')
{
let mut wide: [Option<i32>; 2] = [None, None];
for (slot, s) in wide.iter_mut().zip(args.split(',')) {
*slot = s.trim().parse::<i32>().ok();
}
let nums: [u8; 2] = [
wide[0].and_then(|v| u8::try_from(v).ok()).unwrap_or(0),
wide[1].and_then(|v| u8::try_from(v).ok()).unwrap_or(0),
];
match head {
"bit" => {
return Some(DataType::Bit(
u32::try_from(wide.first().copied().flatten()?).ok()?,
));
}
"varbit" | "bit varying" => {
return Some(DataType::BitVarying(
u32::try_from(wide.first().copied().flatten()?).ok()?,
));
}
"numeric" | "decimal" => {
let precision = u16::try_from(wide.first().copied().flatten()?).ok()?;
let scale = i16::try_from(wide.get(1).copied().flatten().unwrap_or(0)).ok()?;
if !numeric_typmod_in_range(precision, scale) {
return None;
}
return Some(DataType::Numeric { precision, scale });
}
"varchar" => {
return Some(DataType::Varchar(nums.first().copied().unwrap_or(0).into()));
}
"char" | "character" => {
return Some(DataType::Char(nums.first().copied().unwrap_or(0).into()));
}
_ => {}
}
}
Some(match n {
"smallint" | "int2" => DataType::SmallInt,
"numeric" | "decimal" => DataType::Numeric {
precision: 0,
scale: 0,
},
"inet" => DataType::Inet,
"cidr" => DataType::Cidr,
"macaddr" => DataType::Macaddr,
"macaddr8" => DataType::Macaddr8,
"pg_lsn" => DataType::PgLsn,
"__bit_literal" => DataType::BitVarying(0),
"xid" => DataType::Xid,
"xid8" => DataType::Xid8,
"bit" => DataType::Bit(0),
"varbit" | "bit varying" => DataType::BitVarying(0),
"xml" => DataType::Xml,
"tsvector" => DataType::TsVector,
"tsquery" => DataType::TsQuery,
"money" => DataType::Money,
"char1" => DataType::Char1,
"point" => DataType::Point,
"lseg" => DataType::Lseg,
"path" => DataType::Path,
"box" => DataType::PgBox,
"polygon" => DataType::Polygon,
"line" => DataType::Line,
"circle" => DataType::Circle,
"int4multirange" => DataType::Multirange(spg_storage::RangeKind::Int4),
"int8multirange" => DataType::Multirange(spg_storage::RangeKind::Int8),
"nummultirange" => DataType::Multirange(spg_storage::RangeKind::Num),
"tsmultirange" => DataType::Multirange(spg_storage::RangeKind::Ts),
"tstzmultirange" => DataType::Multirange(spg_storage::RangeKind::TsTz),
"datemultirange" => DataType::Multirange(spg_storage::RangeKind::Date),
"int4range" => DataType::Range(spg_storage::RangeKind::Int4),
"int8range" => DataType::Range(spg_storage::RangeKind::Int8),
"numrange" => DataType::Range(spg_storage::RangeKind::Num),
"tsrange" => DataType::Range(spg_storage::RangeKind::Ts),
"tstzrange" => DataType::Range(spg_storage::RangeKind::TsTz),
"daterange" => DataType::Range(spg_storage::RangeKind::Date),
"bool_array" | "boolean_array" => DataType::BoolArray,
"smallint_array" | "int2_array" => DataType::SmallIntArray,
"int_array" | "integer_array" | "int4_array" => DataType::IntArray,
"bigint_array" | "int8_array" => DataType::BigIntArray,
"float_array" | "double_array" | "real_array" | "float8_array" | "float4_array" => {
DataType::FloatArray
}
"float4" | "real" => DataType::Real,
"float8" | "double precision" | "float" => DataType::Float,
"oid" => DataType::Oid,
"oid_array" => DataType::OidArray,
"name_array" | "regtype_array" | "regclass_array" | "regproc_array" => DataType::TextArray,
"time" | "time without time zone" => DataType::Time,
"timetz" | "time with time zone" => DataType::TimeTz,
"hstore" => DataType::Hstore,
"numeric_array" | "decimal_array" => DataType::NumericArray,
"varchar_array" | "character varying_array" | "char_array" | "bpchar_array" => {
DataType::TextArray
}
"text_array" => DataType::TextArray,
"date_array" => DataType::DateArray,
"timestamp_array" => DataType::TimestampArray,
"timestamptz_array" => DataType::TimestamptzArray,
"uuid_array" => DataType::UuidArray,
"json_array" => DataType::JsonArray,
"jsonb_array" => DataType::JsonbArray,
"bytea_array" => DataType::BytesArray,
"interval_array" => DataType::IntervalArray,
"money_array" => DataType::MoneyArray,
"int" | "int4" | "integer" => DataType::Int,
"bigint" | "int8" => DataType::BigInt,
"text" => DataType::Text,
"name" => DataType::Name,
"varchar" | "character varying" => DataType::Varchar(0),
"char" | "character" => DataType::Char(1),
"bpchar" => DataType::Char(0),
"bool" | "boolean" => DataType::Bool,
"date" => DataType::Date,
"timestamp" | "timestamp without time zone" => DataType::Timestamp,
"timestamptz" | "timestamp with time zone" => DataType::Timestamptz,
"uuid" => DataType::Uuid,
"json" => DataType::Json,
"jsonb" => DataType::Jsonb,
"bytea" => DataType::Bytes,
"interval" => DataType::Interval,
_ => return None,
})
}
pub(crate) const fn column_type_to_data_type(t: ColumnTypeName) -> DataType {
match t {
ColumnTypeName::SmallInt => DataType::SmallInt,
ColumnTypeName::Int => DataType::Int,
ColumnTypeName::BigInt => DataType::BigInt,
ColumnTypeName::Float => DataType::Float,
ColumnTypeName::Real => DataType::Real,
ColumnTypeName::Text => DataType::Text,
ColumnTypeName::Name => DataType::Name,
ColumnTypeName::Xid => DataType::Xid,
ColumnTypeName::Xid8 => DataType::Xid8,
ColumnTypeName::Oid => DataType::Oid,
ColumnTypeName::Varchar(n) => DataType::Varchar(n),
ColumnTypeName::Char(n) => DataType::Char(n),
ColumnTypeName::Bool => DataType::Bool,
ColumnTypeName::Vector { dim, encoding } => DataType::Vector {
dim,
encoding: match encoding {
SqlVecEncoding::F32 => VecEncoding::F32,
SqlVecEncoding::Sq8 => VecEncoding::Sq8,
SqlVecEncoding::F16 => VecEncoding::F16,
},
},
ColumnTypeName::Numeric(precision, scale) => DataType::Numeric { precision, scale },
ColumnTypeName::Date => DataType::Date,
ColumnTypeName::Timestamp => DataType::Timestamp,
ColumnTypeName::Timestamptz => DataType::Timestamptz,
ColumnTypeName::Json => DataType::Json,
ColumnTypeName::Jsonb => DataType::Jsonb,
ColumnTypeName::Bytes => DataType::Bytes,
ColumnTypeName::TextArray => DataType::TextArray,
ColumnTypeName::IntArray => DataType::IntArray,
ColumnTypeName::BigIntArray => DataType::BigIntArray,
ColumnTypeName::TsVector => DataType::TsVector,
ColumnTypeName::TsQuery => DataType::TsQuery,
ColumnTypeName::Uuid => DataType::Uuid,
ColumnTypeName::Time => DataType::Time,
ColumnTypeName::Year => DataType::Year,
ColumnTypeName::TimeTz => DataType::TimeTz,
ColumnTypeName::Money => DataType::Money,
ColumnTypeName::Range(k) => DataType::Range(match k {
spg_sql::ast::RangeKindAst::Int4 => spg_storage::RangeKind::Int4,
spg_sql::ast::RangeKindAst::Int8 => spg_storage::RangeKind::Int8,
spg_sql::ast::RangeKindAst::Num => spg_storage::RangeKind::Num,
spg_sql::ast::RangeKindAst::Ts => spg_storage::RangeKind::Ts,
spg_sql::ast::RangeKindAst::TsTz => spg_storage::RangeKind::TsTz,
spg_sql::ast::RangeKindAst::Date => spg_storage::RangeKind::Date,
}),
ColumnTypeName::Hstore => DataType::Hstore,
ColumnTypeName::IntArray2D => DataType::IntArray2D,
ColumnTypeName::BigIntArray2D => DataType::BigIntArray2D,
ColumnTypeName::TextArray2D => DataType::TextArray2D,
ColumnTypeName::BoolArray2D => DataType::BoolArray2D,
ColumnTypeName::Interval => DataType::Interval,
ColumnTypeName::IntervalArray => DataType::IntervalArray,
ColumnTypeName::BoolArray => DataType::BoolArray,
ColumnTypeName::SmallIntArray => DataType::SmallIntArray,
ColumnTypeName::FloatArray => DataType::FloatArray,
ColumnTypeName::NumericArray => DataType::NumericArray,
ColumnTypeName::DateArray => DataType::DateArray,
ColumnTypeName::TimestampArray => DataType::TimestampArray,
ColumnTypeName::TimestamptzArray => DataType::TimestamptzArray,
ColumnTypeName::UuidArray => DataType::UuidArray,
ColumnTypeName::JsonArray => DataType::JsonArray,
ColumnTypeName::JsonbArray => DataType::JsonbArray,
ColumnTypeName::BytesArray => DataType::BytesArray,
ColumnTypeName::VarcharArray => DataType::VarcharArray,
ColumnTypeName::CharArray => DataType::CharArray,
ColumnTypeName::Multirange(k) => DataType::Multirange(match k {
spg_sql::ast::RangeKindAst::Int4 => spg_storage::RangeKind::Int4,
spg_sql::ast::RangeKindAst::Int8 => spg_storage::RangeKind::Int8,
spg_sql::ast::RangeKindAst::Num => spg_storage::RangeKind::Num,
spg_sql::ast::RangeKindAst::Ts => spg_storage::RangeKind::Ts,
spg_sql::ast::RangeKindAst::TsTz => spg_storage::RangeKind::TsTz,
spg_sql::ast::RangeKindAst::Date => spg_storage::RangeKind::Date,
}),
ColumnTypeName::Point => DataType::Point,
ColumnTypeName::Lseg => DataType::Lseg,
ColumnTypeName::Path => DataType::Path,
ColumnTypeName::PgBox => DataType::PgBox,
ColumnTypeName::Polygon => DataType::Polygon,
ColumnTypeName::Line => DataType::Line,
ColumnTypeName::Circle => DataType::Circle,
ColumnTypeName::Inet => DataType::Inet,
ColumnTypeName::Cidr => DataType::Cidr,
ColumnTypeName::Macaddr => DataType::Macaddr,
ColumnTypeName::Macaddr8 => DataType::Macaddr8,
ColumnTypeName::Bit(n) => DataType::Bit(n),
ColumnTypeName::BitVarying(n) => DataType::BitVarying(n),
ColumnTypeName::Xml => DataType::Xml,
ColumnTypeName::Char1 => DataType::Char1,
ColumnTypeName::MoneyArray => DataType::MoneyArray,
}
}
pub(crate) fn literal_expr_to_value(expr: Expr) -> Result<Value<'static>, EngineError> {
literal_expr_to_value_in(expr, None)
}
pub(crate) fn literal_expr_to_value_in(
expr: Expr,
catalog: Option<&spg_storage::Catalog>,
) -> Result<Value<'static>, EngineError> {
match expr {
Expr::Literal(l) => Ok(literal_to_value(l)),
Expr::Cast { expr, target } => {
if catalog.is_some()
&& matches!(
target,
spg_sql::ast::CastTarget::Named(_) | spg_sql::ast::CastTarget::RegClass
)
{
return eval_expr_with_catalog(Expr::Cast { expr, target }, catalog);
}
let inner_value = literal_expr_to_value_in(*expr, catalog)?;
crate::eval::cast_value(inner_value, target).map_err(EngineError::Eval)
}
Expr::Unary {
op: UnOp::Neg,
expr,
} => match *expr {
Expr::Literal(Literal::Integer(n)) => {
let neg = n.checked_neg().ok_or_else(|| {
EngineError::Unsupported("integer literal overflow on negation".into())
})?;
Ok(int_value_for(neg))
}
Expr::Literal(Literal::Float(x)) => Ok(Value::Float(-x)),
Expr::Literal(Literal::Numeric { unscaled, scale }) => Ok(Value::Numeric {
scaled: -unscaled,
scale,
kind: spg_storage::NumericKind::Finite,
}),
Expr::Literal(Literal::NumericBig(ref s)) => {
let flipped = if let Some(rest) = s.strip_prefix('-') {
rest.to_string()
} else {
alloc::format!("-{s}")
};
Ok(big_literal_to_value(&flipped))
}
Expr::Cast {
expr: inner,
target,
} => {
let negated_inner = match *inner {
Expr::Literal(Literal::Integer(n)) => {
let neg = n.checked_neg().ok_or_else(|| {
EngineError::Unsupported("integer literal overflow on negation".into())
})?;
Expr::Literal(Literal::Integer(neg))
}
Expr::Literal(Literal::Float(x)) => Expr::Literal(Literal::Float(-x)),
Expr::Literal(Literal::Numeric { unscaled, scale }) => {
Expr::Literal(Literal::Numeric {
unscaled: -unscaled,
scale,
})
}
Expr::Literal(Literal::NumericBig(ref s)) => {
let flipped = if let Some(rest) = s.strip_prefix('-') {
rest.to_string()
} else {
alloc::format!("-{s}")
};
Expr::Literal(Literal::NumericBig(flipped))
}
other => Expr::Unary {
op: spg_sql::ast::UnOp::Neg,
expr: alloc::boxed::Box::new(other),
},
};
literal_expr_to_value_in(
Expr::Cast {
expr: alloc::boxed::Box::new(negated_inner),
target,
},
catalog,
)
}
other => Err(EngineError::Unsupported(alloc::format!(
"unary minus over non-literal expression: {other:?}"
))),
},
Expr::Array(items) => {
let mut materialised: alloc::vec::Vec<Value<'static>> =
alloc::vec::Vec::with_capacity(items.len());
for elem in &items {
materialised.push(literal_expr_to_value_in(elem.clone(), catalog)?);
}
Ok(crate::describe::upgrade_timestamptz_array(
array_literal_widen(materialised),
&items,
&[],
))
}
other => eval_expr_with_catalog(other, catalog),
}
}
fn eval_expr_with_catalog(
expr: Expr,
catalog: Option<&spg_storage::Catalog>,
) -> Result<Value<'static>, EngineError> {
let empty_schema: alloc::vec::Vec<spg_storage::ColumnSchema> = alloc::vec::Vec::new();
let mut ctx = EvalContext::new(&empty_schema, None);
if let Some(cat) = catalog {
ctx = ctx.with_catalog(cat);
}
let empty_row = spg_storage::Row::new(alloc::vec::Vec::new());
crate::eval::eval_expr(&expr, &empty_row, &ctx).map_err(EngineError::Eval)
}
pub(crate) fn literal_to_value(l: Literal) -> Value<'static> {
match l {
Literal::Integer(n) => int_value_for(n),
Literal::Float(x) => Value::Float(x),
Literal::Numeric { unscaled, scale } => Value::Numeric {
scaled: unscaled,
scale,
kind: spg_storage::NumericKind::Finite,
},
Literal::NumericBig(s) => big_literal_to_value(&s),
Literal::String(s) => Value::text(s),
Literal::Bool(b) => Value::Bool(b),
Literal::Null => Value::Null,
Literal::Vector(v) => Value::vector(v),
Literal::TextArray(items) => Value::TextArray(items),
Literal::IntArray(items) => Value::IntArray(items),
Literal::BigIntArray(items) => Value::BigIntArray(items),
Literal::Interval {
months,
days,
micros,
..
} => Value::Interval {
months,
days,
micros,
},
}
}
pub(crate) fn int_value_for(n: i64) -> Value<'static> {
if let Ok(small) = i32::try_from(n) {
Value::Int(small)
} else {
Value::BigInt(n)
}
}
pub(crate) fn truncate_to_column_fsp(v: Value<'static>, schema: &ColumnSchema) -> Value<'static> {
let Some(fsp) = schema.mysql_fsp else {
return v;
};
if fsp >= 6 {
return v;
}
let scale = 10i64.pow(u32::from(6 - fsp));
let cut = |micros: i64| (micros / scale) * scale;
match v {
Value::Timestamp(m) => Value::Timestamp(cut(m)),
Value::Time(m) => Value::Time(cut(m)),
other => other,
}
}
fn column_int_bounds(schema: &ColumnSchema) -> Option<(i128, i128)> {
if let Some(width) = schema.mysql_int_width {
return Some(match (width, schema.is_unsigned) {
(spg_storage::MysqlIntWidth::Tiny, false) => (-128, 127),
(spg_storage::MysqlIntWidth::Tiny, true) => (0, 255),
(spg_storage::MysqlIntWidth::Small, false) => (-32_768, 32_767),
(spg_storage::MysqlIntWidth::Small, true) => (0, 65_535),
(spg_storage::MysqlIntWidth::Medium, false) => (-8_388_608, 8_388_607),
(spg_storage::MysqlIntWidth::Medium, true) => (0, 16_777_215),
(spg_storage::MysqlIntWidth::Int, false) => (-2_147_483_648, 2_147_483_647),
(spg_storage::MysqlIntWidth::Int, true) => (0, 4_294_967_295),
(spg_storage::MysqlIntWidth::Big, false) => {
(i128::from(i64::MIN), i128::from(i64::MAX))
}
(spg_storage::MysqlIntWidth::Big, true) => (0, i128::from(u64::MAX)),
});
}
let (lo, hi) = match schema.ty {
DataType::SmallInt => (i128::from(i16::MIN), i128::from(i16::MAX)),
DataType::Int => (i128::from(i32::MIN), i128::from(i32::MAX)),
DataType::BigInt => (i128::from(i64::MIN), i128::from(i64::MAX)),
_ => return None,
};
Some(if schema.is_unsigned {
(0, hi)
} else {
(lo, hi)
})
}
pub(crate) fn mysql_ignore_fit(v: Value<'static>, schema: &ColumnSchema) -> Value<'static> {
if v.is_null() {
if schema.nullable {
return v;
}
return match schema.ty {
DataType::SmallInt | DataType::Int | DataType::BigInt => Value::BigInt(0),
DataType::Float | DataType::Real => Value::Float(0.0),
DataType::Text | DataType::Varchar(_) | DataType::Char(_) => Value::text(""),
_ => v,
};
}
if let Value::Text(ref s) = v
&& matches!(
schema.ty,
DataType::SmallInt | DataType::Int | DataType::BigInt
)
&& s.trim().parse::<i64>().is_err()
{
return Value::BigInt(leading_numeric_prefix(s));
}
let as_int = match v {
Value::SmallInt(n) => Some(i128::from(n)),
Value::Int(n) => Some(i128::from(n)),
Value::BigInt(n) => Some(i128::from(n)),
Value::Numeric {
scaled, scale: 0, ..
} => Some(scaled),
_ => None,
};
if let Some(n) = as_int
&& let Some((lo, hi)) = column_int_bounds(schema)
&& (n < lo || n > hi)
{
return int_value_for_column(n.clamp(lo, hi));
}
if let Value::Text(ref s) = v {
let max = match schema.ty {
DataType::Varchar(m) | DataType::Char(m) if m > 0 => m as usize,
_ => return v,
};
if s.chars().count() > max {
return Value::text(s.chars().take(max).collect::<alloc::string::String>());
}
}
v
}
fn leading_numeric_prefix(s: &str) -> i64 {
let t = s.trim_start();
let b = t.as_bytes();
let mut i = 0;
if i < b.len() && (b[i] == b'-' || b[i] == b'+') {
i += 1;
}
let int_start = i;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
let mut end = i;
if i < b.len() && b[i] == b'.' {
i += 1;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
if i > int_start + 1 {
end = i;
}
}
if end > int_start && i < b.len() && (b[i] == b'e' || b[i] == b'E') {
let mut j = i + 1;
if j < b.len() && (b[j] == b'-' || b[j] == b'+') {
j += 1;
}
let digits_start = j;
while j < b.len() && b[j].is_ascii_digit() {
j += 1;
}
if j > digits_start {
end = j;
}
}
let Ok(f) = t[..end].parse::<f64>() else {
return 0;
};
let r = f.round();
if r >= i64::MAX as f64 {
i64::MAX
} else if r <= i64::MIN as f64 {
i64::MIN
} else {
r as i64
}
}
fn int_value_for_column(n: i128) -> Value<'static> {
match i64::try_from(n) {
Ok(v) => Value::BigInt(v),
Err(_) => Value::numeric(n, 0),
}
}
pub(crate) fn check_unsigned_range(
v: &Value,
schema: &ColumnSchema,
position: usize,
) -> Result<(), EngineError> {
let n: i128 = match v {
Value::SmallInt(x) => i128::from(*x),
Value::Int(x) => i128::from(*x),
Value::BigInt(x) => i128::from(*x),
Value::Numeric { scaled, scale, .. } if *scale == 0 => *scaled,
_ => return Ok(()), };
if let Some(width) = schema.mysql_int_width {
let _ = width;
let (lo, hi) = column_int_bounds(schema).unwrap_or((i128::MIN, i128::MAX));
if n < lo || n > hi {
return Err(EngineError::Unsupported(alloc::format!(
"Out of range value for column '{}'",
schema.name
)));
}
return Ok(());
}
if schema.is_unsigned && n < 0 {
return Err(EngineError::Unsupported(alloc::format!(
"column {:?} is UNSIGNED but got negative value {n} at position {position}",
schema.name
)));
}
Ok(())
}
fn coerce_text_array_to(
items: alloc::vec::Vec<Option<alloc::string::String>>,
target: DataType,
col: &str,
) -> Result<Option<Value<'static>>, EngineError> {
let elem_dt = match target {
DataType::BoolArray => DataType::Bool,
DataType::NumericArray => DataType::Numeric {
precision: 0,
scale: 0,
},
DataType::DateArray => DataType::Date,
DataType::TimestampArray => DataType::Timestamp,
DataType::TimestamptzArray => DataType::Timestamptz,
DataType::UuidArray => DataType::Uuid,
DataType::IntervalArray => DataType::Interval,
_ => return Ok(None),
};
let mut scal: alloc::vec::Vec<Option<Value<'static>>> =
alloc::vec::Vec::with_capacity(items.len());
for item in items {
match item {
None => scal.push(None),
Some(s) => scal.push(Some(coerce_value(Value::text(s), elem_dt, col, 0)?)),
}
}
let out = match target {
DataType::BoolArray => Value::BoolArray(
scal.into_iter()
.map(|o| o.map(|v| matches!(v, Value::Bool(true))))
.collect(),
),
DataType::NumericArray => Value::NumericArray(
scal.into_iter()
.map(|o| {
o.map(|v| match v {
Value::Numeric { scaled, scale, .. } => (scaled, scale),
_ => (0, 0),
})
})
.collect(),
),
DataType::DateArray => Value::DateArray(
scal.into_iter()
.map(|o| {
o.map(|v| match v {
Value::Date(d) => d,
_ => 0,
})
})
.collect(),
),
DataType::TimestampArray => Value::TimestampArray(
scal.into_iter()
.map(|o| {
o.map(|v| match v {
Value::Timestamp(t) => t,
_ => 0,
})
})
.collect(),
),
DataType::TimestamptzArray => Value::TimestamptzArray(
scal.into_iter()
.map(|o| {
o.map(|v| match v {
Value::Timestamp(t) => t,
_ => 0,
})
})
.collect(),
),
DataType::UuidArray => Value::UuidArray(
scal.into_iter()
.map(|o| {
o.map(|v| match v {
Value::Uuid(u) => u,
_ => [0u8; 16],
})
})
.collect(),
),
DataType::IntervalArray => Value::IntervalArray(
scal.into_iter()
.map(|o| {
o.and_then(|v| match v {
Value::Interval {
months,
days,
micros,
} => Some(spg_storage::IntervalSpan {
months,
days,
micros,
}),
_ => None,
})
})
.collect(),
),
_ => return Ok(None),
};
Ok(Some(out))
}
pub(crate) fn array_oid_element(oid: i64) -> Option<i64> {
Some(match oid {
1000 => 16, 1001 => 17, 1002 => 18, 1003 => 19, 1016 => 20, 1005 => 21, 1007 => 23, 1009 => 25, 1028 => 26, 199 => 114, 143 => 142, 651 => 650, 1021 => 700, 1022 => 701, 775 => 774, 791 => 790, 1040 => 829, 1041 => 869, 1014 => 1042, 1015 => 1043, 1182 => 1082, 1183 => 1083, 1115 => 1114, 1185 => 1184, 1187 => 1186, 1270 => 1266, 1561 => 1560, 1563 => 1562, 1231 => 1700, 2951 => 2950, 3643 => 3614, 3645 => 3615, 3807 => 3802, _ => return None,
})
}
pub(crate) fn regtype_oid_to_name_owned(oid: i64) -> Option<alloc::string::String> {
if let Some(scalar) = regtype_oid_to_name(oid) {
return Some(alloc::string::String::from(scalar));
}
let (_, _, elem) = crate::system_catalog::ARRAY_TYPE_OIDS
.iter()
.find(|(arr, _, _)| *arr == oid)?;
Some(alloc::format!("{}[]", regtype_oid_to_name(*elem)?))
}
pub(crate) fn array_oid_for_element(elem: i64) -> Option<i64> {
crate::system_catalog::ARRAY_TYPE_OIDS
.iter()
.find(|(_, _, e)| *e == elem)
.map(|(arr, _, _)| *arr)
}
pub(crate) fn regtype_oid_to_name(oid: i64) -> Option<&'static str> {
Some(match oid {
4600 => "pg_brin_bloom_summary",
16 => "boolean",
17 => "bytea",
18 => "\"char\"",
19 => "name",
20 => "bigint",
21 => "smallint",
23 => "integer",
25 => "text",
26 => "oid",
27 => "tid",
28 => "xid",
29 => "cid",
5069 => "xid8",
114 => "json",
142 => "xml",
650 => "cidr",
700 => "real",
701 => "double precision",
774 => "macaddr8",
790 => "money",
829 => "macaddr",
869 => "inet",
1042 => "character",
1043 => "character varying",
1082 => "date",
1083 => "time without time zone",
1114 => "timestamp without time zone",
1184 => "timestamp with time zone",
1186 => "interval",
1266 => "time with time zone",
1560 => "bit",
1562 => "bit varying",
1700 => "numeric",
2950 => "uuid",
3614 => "tsvector",
3615 => "tsquery",
3802 => "jsonb",
3904 => "int4range",
3906 => "numrange",
3908 => "tsrange",
3910 => "tstzrange",
3912 => "daterange",
3926 => "int8range",
_ => return None,
})
}
pub(crate) fn parse_pg_int(s: &str) -> Option<i64> {
let s = s.trim();
let (neg, rest) = if let Some(r) = s.strip_prefix('-') {
(true, r)
} else if let Some(r) = s.strip_prefix('+') {
(false, r)
} else {
(false, s)
};
let (radix, digits, has_prefix) =
if let Some(h) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) {
(16u32, h, true)
} else if let Some(o) = rest.strip_prefix("0o").or_else(|| rest.strip_prefix("0O")) {
(8, o, true)
} else if let Some(b) = rest.strip_prefix("0b").or_else(|| rest.strip_prefix("0B")) {
(2, b, true)
} else {
(10, rest, false)
};
let db = digits.as_bytes();
if db.last() == Some(&b'_')
|| digits.contains("__")
|| (!has_prefix && db.first() == Some(&b'_'))
{
return None;
}
let cleaned: alloc::string::String = digits.chars().filter(|&c| c != '_').collect();
if cleaned.is_empty() {
return None;
}
let mag = i64::from_str_radix(&cleaned, radix).ok()?;
Some(if neg { mag.checked_neg()? } else { mag })
}
fn xml_content_is_well_formed(s: &str) -> bool {
let b = s.as_bytes();
let is_name =
|c: u8| c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'.' | b':') || c >= 0x80;
let mut stack: alloc::vec::Vec<&[u8]> = alloc::vec::Vec::new();
let mut i = 0;
while i < b.len() {
if b[i] != b'<' {
i += 1;
continue;
}
let rest = &s[i..];
if rest.starts_with("<!--") {
match rest.find("-->") {
Some(p) => i += p + 3,
None => return false,
}
} else if rest.starts_with("<![CDATA[") {
match rest.find("]]>") {
Some(p) => i += p + 3,
None => return false,
}
} else if rest.starts_with("<?") {
match rest.find("?>") {
Some(p) => i += p + 2,
None => return false,
}
} else if rest.starts_with("<!") {
match rest.find('>') {
Some(p) => i += p + 1,
None => return false,
}
} else {
let close = i + 1 < b.len() && b[i + 1] == b'/';
let name_start = if close { i + 2 } else { i + 1 };
let mut j = name_start;
while j < b.len() && is_name(b[j]) {
j += 1;
}
if j == name_start {
return false; }
let name = &b[name_start..j];
let mut k = j;
let mut quote = 0u8;
let mut prev = 0u8;
loop {
if k >= b.len() {
return false; }
let c = b[k];
if quote != 0 {
if c == quote {
quote = 0;
}
} else if c == b'"' || c == b'\'' {
quote = c;
} else if c == b'>' {
break;
}
prev = c;
k += 1;
}
let self_closing = prev == b'/';
i = k + 1;
if close {
match stack.pop() {
Some(top) if top == name => {}
_ => return false,
}
} else if !self_closing {
stack.push(name);
}
}
}
stack.is_empty()
}
pub(crate) fn parse_float8(s: &str) -> Option<f64> {
let t = s.trim();
let parsed = t.parse::<f64>().ok()?;
let body = t.strip_prefix(['+', '-']).unwrap_or(t);
let numeric_looking = body
.bytes()
.next()
.is_some_and(|c| c.is_ascii_digit() || c == b'.');
if numeric_looking {
if parsed.is_infinite() {
return None; }
if parsed == 0.0 {
let mantissa = body.split(['e', 'E']).next().unwrap_or(body);
if mantissa.bytes().any(|c| c.is_ascii_digit() && c != b'0') {
return None;
}
}
}
Some(parsed)
}
fn decode_array_elems(
s: &str,
elem: DataType,
col_name: &str,
position: usize,
) -> Result<Vec<Option<Value<'static>>>, EngineError> {
let raw = decode_text_array_literal(s).map_err(|_| {
EngineError::Eval(EvalError::TypeMismatch {
detail: malformed_array_literal(s),
})
})?;
let mut out = Vec::with_capacity(raw.len());
for e in raw {
match e {
None => out.push(None),
Some(t) => out.push(Some(coerce_value(
Value::text(t),
elem,
col_name,
position,
)?)),
}
}
Ok(out)
}
fn coerce_untyped_value(
v: Value<'static>,
expected: DataType,
col_name: &str,
position: usize,
) -> Result<Value<'static>, EngineError> {
match (&v, expected) {
(
Value::RegClass(oid, _) | Value::RegProc(oid, _) | Value::RegType(oid, _),
DataType::BigInt | DataType::Oid,
) => Ok(Value::BigInt(*oid)),
(
Value::RegClass(oid, _) | Value::RegProc(oid, _) | Value::RegType(oid, _),
DataType::Int,
) => Ok(Value::Int(i32::try_from(*oid).unwrap_or(i32::MAX))),
(
Value::RegClass(_, name) | Value::RegProc(_, name) | Value::RegType(_, name),
DataType::Text,
) => Ok(Value::text(alloc::string::String::from(name.as_ref()))),
(Value::Composite(fields), DataType::Jsonb | DataType::Json) => {
let mut obj = alloc::string::String::from("{");
for (i, (name, val)) in fields.iter().enumerate() {
if i > 0 {
obj.push(',');
}
obj.push_str(&crate::json::value_to_json_text(&Value::text(
alloc::string::String::from(name.as_str()),
)));
obj.push(':');
obj.push_str(&crate::json::value_to_json_text(val));
}
obj.push('}');
Ok(Value::Json(alloc::borrow::Cow::Owned(obj)))
}
(Value::Composite(_), DataType::Text) => Ok(Value::text(crate::eval::value_to_text(&v))),
_ => Err(EngineError::Unsupported(alloc::format!(
"cannot coerce {:?} to {expected:?} for column {col_name:?} (position {position})",
v
))),
}
}
fn invalid_input_syntax(ty: &str, value: &str) -> EngineError {
EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type {ty}: \"{value}\""),
})
}
fn real_out_of_range(value: &str) -> EngineError {
float_out_of_range(value, "real")
}
fn float_out_of_range(value: &str, ty: &str) -> EngineError {
EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("\"{value}\" is out of range for type {ty}"),
})
}
fn float_text_error(s: &str, ty: &str) -> EngineError {
let t = s.trim();
let body = t.strip_prefix(['+', '-']).unwrap_or(t);
let numeric_looking = body
.bytes()
.next()
.is_some_and(|c| c.is_ascii_digit() || c == b'.');
if numeric_looking && t.parse::<f64>().is_ok() {
float_out_of_range(t, ty)
} else {
invalid_input_syntax(ty, s)
}
}
fn float_text_is_nonzero(t: &str) -> bool {
let body = t.strip_prefix(['+', '-']).unwrap_or(t);
let mantissa = body.split(['e', 'E']).next().unwrap_or(body);
mantissa.bytes().any(|c| c.is_ascii_digit() && c != b'0')
}
fn text_is_explicit_infinity(t: &str) -> bool {
let t = t.trim_start_matches(['+', '-']);
t.eq_ignore_ascii_case("inf") || t.eq_ignore_ascii_case("infinity")
}
fn datetime_parse_error(ty: &str, s: &str) -> EngineError {
let t = s.trim();
let date_shaped = t.chars().any(|c| c.is_ascii_digit())
&& t.chars().all(|c| {
c.is_ascii_digit() || matches!(c, '-' | '/' | ':' | '.' | ' ' | '+' | 'T' | 't')
});
let detail = if date_shaped {
alloc::format!("date/time field value out of range: \"{t}\"")
} else {
alloc::format!("invalid input syntax for type {ty}: \"{t}\"")
};
EngineError::Eval(EvalError::TypeMismatch { detail })
}
pub(crate) enum JsonbScalar {
Numeric(Value<'static>),
Bool(bool),
Null,
}
pub(crate) fn jsonb_cast_type_error(kind: &str, target: &str) -> EvalError {
EvalError::TypeMismatch {
detail: alloc::format!("cannot cast jsonb {kind} to type {target}"),
}
}
pub(crate) fn jsonb_scalar_for_cast(s: &str, target: &str) -> Result<JsonbScalar, EvalError> {
use crate::json::JsonValue;
match crate::json::parse(s) {
Ok(JsonValue::Null) => Ok(JsonbScalar::Null),
Ok(JsonValue::Bool(b)) => Ok(JsonbScalar::Bool(b)),
Ok(JsonValue::Number(x)) => {
let num = coerce_value(
Value::text(alloc::format!("{x}")),
DataType::Numeric {
precision: 0,
scale: 0,
},
"",
0,
)
.map_err(|e| match e {
EngineError::Eval(ev) => ev,
_ => jsonb_cast_type_error("numeric", target),
})?;
Ok(JsonbScalar::Numeric(num))
}
Ok(JsonValue::NumberText(text)) => {
let num = coerce_value(
Value::text(text),
DataType::Numeric {
precision: 0,
scale: 0,
},
"",
0,
)
.map_err(|e| match e {
EngineError::Eval(ev) => ev,
_ => jsonb_cast_type_error("numeric", target),
})?;
Ok(JsonbScalar::Numeric(num))
}
Ok(JsonValue::String(_)) => Err(jsonb_cast_type_error("string", target)),
Ok(JsonValue::Array(_)) => Err(jsonb_cast_type_error("array", target)),
Ok(JsonValue::Object(_)) => Err(jsonb_cast_type_error("object", target)),
Err(_) => Err(jsonb_cast_type_error("value", target)),
}
}
pub(crate) fn normalize_composite_for_column(
v: Value<'static>,
col: &ColumnSchema,
catalog: Option<&spg_storage::Catalog>,
) -> Result<Value<'static>, EngineError> {
let Some(tname) = col.user_composite_type.as_deref() else {
return Ok(v);
};
if matches!(v, Value::Null) {
return Ok(v);
}
let Some(def) = catalog.and_then(|c| c.composite_types().get(tname)) else {
return Ok(v);
};
if matches!(v, Value::Json(_)) {
return Ok(v);
}
crate::eval::apply_composite_cast_pub(v, def, catalog).map_err(EngineError::Eval)
}
fn try_coerce_json_scalar(
s: &str,
expected: DataType,
col_name: &str,
position: usize,
) -> Option<Result<Value<'static>, EngineError>> {
let target = match expected {
DataType::Int => "integer",
DataType::BigInt => "bigint",
DataType::SmallInt => "smallint",
DataType::Numeric { .. } => "numeric",
DataType::Real => "real",
DataType::Float => "double precision",
DataType::Bool => "boolean",
_ => return None,
};
Some(
(|| match jsonb_scalar_for_cast(s, target).map_err(EngineError::Eval)? {
JsonbScalar::Null => Ok(Value::Null),
JsonbScalar::Bool(b) => {
if matches!(expected, DataType::Bool) {
Ok(Value::Bool(b))
} else {
Err(EngineError::Eval(jsonb_cast_type_error("boolean", target)))
}
}
JsonbScalar::Numeric(n) => {
if matches!(expected, DataType::Bool) {
Err(EngineError::Eval(jsonb_cast_type_error("numeric", target)))
} else {
coerce_value(n, expected, col_name, position)
}
}
})(),
)
}
pub(crate) fn mysql_bytes_for_column(
v: Value<'static>,
expected: DataType,
mysql: bool,
) -> Value<'static> {
if !mysql {
return v;
}
let Value::Bytes(ref b) = v else {
return v;
};
match expected {
DataType::SmallInt
| DataType::Int
| DataType::BigInt
| DataType::Float
| DataType::Real
| DataType::Numeric { .. } => {
let start = b.len().saturating_sub(16);
let acc = b[start..]
.iter()
.fold(0u128, |a, &x| (a << 8) | u128::from(x));
if acc <= i64::MAX as u128 {
#[allow(clippy::cast_possible_truncation)]
Value::BigInt(acc as i64)
} else {
big_literal_to_value(&alloc::format!("{acc}"))
}
}
DataType::Text | DataType::Varchar(_) | DataType::Char(_) => Value::text(
b.iter()
.map(|&x| x as char)
.collect::<alloc::string::String>(),
),
_ => v,
}
}
fn try_coerce_time_family(
v: &Value<'static>,
expected: DataType,
) -> Option<Result<Value<'static>, EngineError>> {
const DAY_US: i64 = 86_400_000_000;
if expected != DataType::Time {
return None;
}
match v {
Value::TimeTz { us, .. } => Some(Ok(Value::Time(*us))),
Value::Interval { micros, .. } => Some(Ok(Value::Time(micros.rem_euclid(DAY_US)))),
_ => None,
}
}
pub(crate) fn coerce_to_oid(v: &Value<'_>) -> Result<Option<Value<'static>>, EvalError> {
let as_i64 = match v {
Value::Null => return Ok(Some(Value::Null)),
Value::SmallInt(n) => i64::from(*n),
Value::Int(n) => i64::from(*n),
Value::BigInt(n) => *n,
Value::Text(t) => match t.trim().parse::<i64>() {
Ok(n) => n,
Err(_) => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type oid: {:?}", t.trim()),
});
}
},
_ => return Ok(None),
};
if (-(1i64 << 31)..0).contains(&as_i64) {
return Ok(Some(Value::BigInt(as_i64 + (1i64 << 32))));
}
if !(0..=i64::from(u32::MAX)).contains(&as_i64) {
return Err(EvalError::TypeMismatch {
detail: "OID out of range".into(),
});
}
Ok(Some(Value::BigInt(as_i64)))
}
pub(crate) fn coerce_value(
v: Value<'static>,
expected: DataType,
col_name: &str,
position: usize,
) -> Result<Value<'static>, EngineError> {
if v.is_null() {
return Ok(Value::Null);
}
if let Value::Json(ref s) = v {
if let Some(res) = try_coerce_json_scalar(s, expected, col_name, position) {
return res;
}
}
if let Some(res) = try_coerce_time_family(&v, expected) {
return res;
}
if let Value::Numeric { kind, .. } = v
&& kind != spg_storage::NumericKind::Finite
{
use spg_storage::NumericKind as K;
let as_f64 = match kind {
K::NaN => f64::NAN,
K::PosInf => f64::INFINITY,
K::NegInf => f64::NEG_INFINITY,
K::Finite => unreachable!("checked above"),
};
let what = if kind == K::NaN { "NaN" } else { "infinity" };
let int_err = |target: &str| {
Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("cannot convert {what} to {target}"),
}))
};
match expected {
DataType::Float => return Ok(Value::Float(as_f64)),
#[allow(clippy::cast_possible_truncation)]
DataType::Real => return Ok(Value::Real(as_f64 as f32)),
DataType::Int => return int_err("integer"),
DataType::BigInt => return int_err("bigint"),
DataType::SmallInt => return int_err("smallint"),
DataType::Numeric { precision, scale } => {
if precision != 0 && kind != K::NaN {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::string::String::from("numeric field overflow"),
}));
}
let _ = scale;
return Ok(v);
}
_ => {}
}
}
if let DataType::Numeric { precision, .. } = expected {
let f = match v {
Value::Float(f) if !f.is_finite() => Some(f),
#[allow(clippy::cast_lossless)]
Value::Real(f) if !f.is_finite() => Some(f as f64),
_ => None,
};
if let Some(f) = f {
use spg_storage::NumericKind as K;
if f.is_nan() {
return Ok(Value::numeric_special(K::NaN));
}
if precision != 0 {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::string::String::from("numeric field overflow"),
}));
}
return Ok(Value::numeric_special(if f > 0.0 {
K::PosInf
} else {
K::NegInf
}));
}
}
let Some(actual) = v.data_type() else {
return coerce_untyped_value(v, expected, col_name, position);
};
if actual == expected {
return Ok(v);
}
let coerced: Option<Value<'static>> = match (v, expected) {
(Value::Int(n), DataType::BigInt) => Some(Value::BigInt(i64::from(n))),
(Value::Int(n), DataType::Float) => Some(Value::Float(f64::from(n))),
(Value::Int(n), DataType::SmallInt) => match i16::try_from(n) {
Ok(v) => Some(Value::SmallInt(v)),
Err(_) => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: "smallint out of range".into(),
}));
}
},
(Value::Int(n), DataType::Numeric { precision, scale }) => Some(numeric_from_integer(
i128::from(n),
precision,
scale,
col_name,
)?),
(Value::SmallInt(n), DataType::Int) => Some(Value::Int(i32::from(n))),
(Value::SmallInt(n), DataType::BigInt) => Some(Value::BigInt(i64::from(n))),
(Value::SmallInt(n), DataType::Float) => Some(Value::Float(f64::from(n))),
(Value::SmallInt(n), DataType::Numeric { precision, scale }) => Some(numeric_from_integer(
i128::from(n),
precision,
scale,
col_name,
)?),
(Value::BigInt(n), DataType::Int) => match i32::try_from(n) {
Ok(v) => Some(Value::Int(v)),
Err(_) => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: "integer out of range".into(),
}));
}
},
(Value::BigInt(n), DataType::SmallInt) => match i16::try_from(n) {
Ok(v) => Some(Value::SmallInt(v)),
Err(_) => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: "smallint out of range".into(),
}));
}
},
#[allow(clippy::cast_precision_loss)]
(Value::BigInt(n), DataType::Float) => Some(Value::Float(n as f64)),
(Value::BigInt(n), DataType::Numeric { precision, scale }) => Some(numeric_from_integer(
i128::from(n),
precision,
scale,
col_name,
)?),
(Value::Float(x), DataType::Numeric { precision, scale }) => {
if precision == 0 && scale == 0 && x.is_finite() {
if let Some((mantissa, src_scale)) = parse_numeric_text(&alloc::format!("{x}")) {
Some(Value::Numeric {
scaled: mantissa,
scale: src_scale,
kind: spg_storage::NumericKind::Finite,
})
} else {
Some(numeric_from_float(x, precision, scale, col_name)?)
}
} else {
Some(numeric_from_float(x, precision, scale, col_name)?)
}
}
(Value::Real(x), DataType::Numeric { precision, scale }) => {
if precision == 0 && scale == 0 && x.is_finite() {
let six = alloc::format!("{:.5e}", x);
let six: f64 = six.parse().unwrap_or_else(|_| f64::from(x));
if let Some((mantissa, src_scale)) = parse_numeric_text(&alloc::format!("{six}")) {
Some(Value::Numeric {
scaled: mantissa,
scale: src_scale,
kind: spg_storage::NumericKind::Finite,
})
} else {
Some(numeric_from_float(
f64::from(x),
precision,
scale,
col_name,
)?)
}
} else {
Some(numeric_from_float(
f64::from(x),
precision,
scale,
col_name,
)?)
}
}
(Value::Text(s), DataType::Numeric { precision, scale }) => {
if let Some(kind) = crate::numeric::parse_numeric_special(&s) {
return Ok(Value::numeric_special(kind));
}
let Some((mantissa, src_scale)) = parse_numeric_text(&s) else {
match spg_sql::parser::expand_scientific_literal(&s) {
spg_sql::parser::SciExpanded::Expanded(plain) => {
return coerce_value(
Value::Text(plain.into()),
DataType::Numeric { precision, scale },
col_name,
position,
);
}
spg_sql::parser::SciExpanded::Overflow => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: "value overflows numeric format".into(),
}));
}
spg_sql::parser::SciExpanded::NotScientific => {}
}
if precision == 0 && scale == 0 {
if let Some(b) = spg_storage::bignum::BigNumeric::from_decimal_str(&s) {
return Ok(Value::NumericBig(alloc::boxed::Box::new(b)));
}
}
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type numeric: \"{s}\""),
}));
};
if precision == 0 && scale == 0 {
Some(Value::Numeric {
scaled: mantissa,
scale: src_scale,
kind: spg_storage::NumericKind::Finite,
})
} else {
Some(numeric_rescale(
mantissa, src_scale, precision, scale, col_name,
)?)
}
}
(Value::Text(s), DataType::Date) => {
let d = eval::parse_date_literal(&s)
.or_else(|| {
eval::parse_timestamp_literal(&s)
.and_then(|t| i32::try_from(t.div_euclid(86_400_000_000)).ok())
})
.ok_or_else(|| datetime_parse_error("date", &s))?;
Some(Value::Date(d))
}
(Value::Text(s), DataType::SmallInt) => Some(Value::SmallInt(
parse_pg_int(&s)
.and_then(|n| i16::try_from(n).ok())
.ok_or_else(|| invalid_input_syntax("smallint", &s))?,
)),
(Value::Text(s), DataType::Int) => Some(Value::Int(
parse_pg_int(&s)
.and_then(|n| i32::try_from(n).ok())
.ok_or_else(|| invalid_input_syntax("integer", &s))?,
)),
(Value::Text(s), DataType::BigInt) => Some(Value::BigInt(
parse_pg_int(&s).ok_or_else(|| invalid_input_syntax("bigint", &s))?,
)),
(Value::Text(s), DataType::Xid) => Some(Value::Xid(
s.parse::<u32>()
.map_err(|_| invalid_input_syntax("xid", &s))?,
)),
(Value::Xid(x), DataType::Xid) => Some(Value::Xid(x)),
(Value::Text(s), DataType::Xid8) => Some(Value::BigInt(
parse_pg_int(&s).ok_or_else(|| invalid_input_syntax("xid8", &s))?,
)),
(Value::BigInt(n), DataType::Xid8) => Some(Value::BigInt(n)),
(ref other, DataType::Oid) => coerce_to_oid(other)?,
(Value::Text(s), DataType::Float) => {
Some(Value::Float(
parse_float8(&s).ok_or_else(|| float_text_error(&s, "double precision"))?,
))
}
(Value::Int(n), DataType::Real) => Some(Value::Real(n as f32)),
(Value::SmallInt(n), DataType::Real) => Some(Value::Real(f32::from(n))),
(Value::BigInt(n), DataType::Real) => Some(Value::Real(n as f32)),
(Value::Float(x), DataType::Real) => {
let narrowed = x as f32;
if narrowed.is_infinite() && x.is_finite() {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: "value out of range: overflow".into(),
}));
}
if narrowed == 0.0 && x != 0.0 {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: "value out of range: underflow".into(),
}));
}
Some(Value::Real(narrowed))
}
(
Value::Numeric {
scaled,
scale,
kind,
},
DataType::Real,
) => Some(Value::Real(match kind {
spg_storage::NumericKind::NaN => f32::NAN,
spg_storage::NumericKind::PosInf => f32::INFINITY,
spg_storage::NumericKind::NegInf => f32::NEG_INFINITY,
spg_storage::NumericKind::Finite => {
let mut div = 1.0f64;
for _ in 0..scale {
div *= 10.0;
}
let x = (scaled as f64 / div) as f32;
if x == 0.0 && scaled != 0 {
return Err(real_out_of_range(&crate::eval::format_numeric(
scaled, scale,
)));
}
x
}
})),
(Value::Real(x), DataType::Float) => Some(Value::Float(f64::from(x))),
(Value::Text(s), DataType::Real) => {
let t = s.trim();
let x = t
.parse::<f32>()
.ok()
.ok_or_else(|| invalid_input_syntax("real", &s))?;
if x.is_infinite() && !text_is_explicit_infinity(t) {
return Err(real_out_of_range(t));
}
if x == 0.0 && float_text_is_nonzero(t) {
return Err(real_out_of_range(t));
}
Some(Value::Real(x))
}
(Value::Text(s), DataType::Bool) => match s.trim().to_ascii_lowercase().as_str() {
"0" | "f" | "fa" | "fal" | "fals" | "false" | "n" | "no" | "of" | "off" => {
Some(Value::Bool(false))
}
"1" | "t" | "tr" | "tru" | "true" | "y" | "ye" | "yes" | "on" => {
Some(Value::Bool(true))
}
_ => return Err(invalid_input_syntax("boolean", &s)),
},
(Value::Int(n), DataType::Bool) => Some(Value::Bool(n != 0)),
(Value::SmallInt(n), DataType::Bool) => Some(Value::Bool(n != 0)),
(Value::BigInt(n), DataType::Bool) => Some(Value::Bool(n != 0)),
(Value::Text(s), DataType::Json) => Some(Value::json(s)),
(Value::Text(s), DataType::Jsonb) => Some(Value::json(
crate::json::canonicalize_jsonb(s.as_ref()).unwrap_or_else(|_| s.into_owned()),
)),
(Value::Json(s), DataType::Text) => Some(Value::text(s)),
(Value::Json(s), DataType::Json) => Some(Value::json(s)),
(Value::Json(s), DataType::Jsonb) => Some(Value::json(
crate::json::canonicalize_jsonb(s.as_ref()).unwrap_or_else(|_| s.into_owned()),
)),
(Value::Text(s), DataType::Bytes) => {
let bytes = decode_bytea_literal(&s)
.map_err(|e| EngineError::Eval(EvalError::TypeMismatch { detail: e }))?;
Some(Value::bytes(bytes))
}
(Value::Bytes(b), DataType::Text) => Some(Value::text(encode_bytea_hex(&b))),
(Value::Text(s), DataType::Uuid) => match spg_storage::parse_uuid_str(&s) {
Some(b) => Some(Value::Uuid(b)),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type uuid: {s:?}"),
}));
}
},
(Value::Uuid(b), DataType::Text) => Some(Value::text(spg_storage::format_uuid(&b))),
(Value::Text(s), DataType::Time) => match parse_time_str(&s) {
Some(us) => Some(Value::Time(us)),
None => {
let time_shaped = {
let core = s.trim().split('.').next().unwrap_or("");
!core.is_empty()
&& core.split(':').count() >= 2
&& core
.split(':')
.all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()))
};
let detail = if time_shaped {
alloc::format!("date/time field value out of range: {s:?}")
} else {
alloc::format!("invalid input syntax for type time: {s:?}")
};
return Err(EngineError::Eval(EvalError::TypeMismatch { detail }));
}
},
(Value::Time(us), DataType::Text) => Some(Value::text(eval::format_time(us))),
(Value::SmallInt(n), DataType::Year) => Some(coerce_int_to_year(i64::from(n), col_name)?),
(Value::Int(n), DataType::Year) => Some(coerce_int_to_year(i64::from(n), col_name)?),
(Value::BigInt(n), DataType::Year) => Some(coerce_int_to_year(n, col_name)?),
(Value::Text(s), DataType::Year) => match s.trim().parse::<i64>() {
Ok(n) => Some(coerce_int_to_year(n, col_name)?),
Err(_) => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type year: {s:?}"),
}));
}
},
(Value::Year(y), DataType::Text) => Some(Value::text(alloc::format!("{y:04}"))),
(Value::Time(t), DataType::TimeTz) => Some(Value::TimeTz {
us: t,
offset_secs: 0,
}),
(Value::Timestamp(t), DataType::TimeTz) => Some(Value::TimeTz {
us: t.rem_euclid(86_400_000_000),
offset_secs: 0,
}),
(Value::Text(s), DataType::TimeTz) => {
match parse_timetz_str(&s).or_else(|| parse_time_str(s.trim()).map(|us| (us, 0))) {
Some((us, offset_secs)) => Some(Value::TimeTz { us, offset_secs }),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!(
"invalid input syntax for type time with time zone: \
{s:?}"
),
}));
}
}
}
(Value::TimeTz { us, offset_secs }, DataType::Text) => {
Some(Value::text(eval::format_timetz(us, offset_secs)))
}
(Value::Text(s), DataType::Money) => match parse_money_str(&s) {
Some(c) => Some(Value::Money(c)),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type money: {s:?}"),
}));
}
},
(Value::SmallInt(n), DataType::Money) => {
Some(Value::Money(i64::from(n).saturating_mul(100)))
}
(Value::Int(n), DataType::Money) => Some(Value::Money(i64::from(n).saturating_mul(100))),
(Value::BigInt(n), DataType::Money) => Some(Value::Money(n.saturating_mul(100))),
(Value::Float(x), DataType::Money) => {
let scaled = x * 100.0;
let cents = if scaled >= 0.0 {
(scaled + 0.5) as i64
} else {
(scaled - 0.5) as i64
};
Some(Value::Money(cents))
}
(Value::Numeric { scaled, scale, .. }, DataType::Money) => {
let cents = if scale == 2 {
scaled
} else if scale < 2 {
let mult = 10_i128.pow(u32::from(2 - scale));
scaled.saturating_mul(mult)
} else {
let div = 10_i128.pow(u32::from(scale - 2));
let half = div / 2;
let bias = if scaled >= 0 { half } else { -half };
(scaled + bias) / div
};
Some(Value::Money(i64::try_from(cents).unwrap_or(i64::MAX)))
}
(Value::Money(c), DataType::Text) => Some(Value::text(eval::format_money(c))),
(Value::Money(c), DataType::Numeric { .. }) => Some(Value::Numeric {
scaled: i128::from(c),
scale: 2,
kind: spg_storage::NumericKind::Finite,
}),
(Value::Text(s), DataType::Range(kind)) => match parse_range_str(&s, kind) {
Ok(v) => Some(v),
Err(RangeParseError::Misordered) => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::string::String::from(
"range lower bound must be less than or equal to range upper bound",
),
}));
}
Err(RangeParseError::Malformed) => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("malformed range literal: \"{s}\""),
}));
}
Err(RangeParseError::BadElement(bad)) => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!(
"invalid input syntax for type {}: \"{bad}\"",
range_element_type_name(kind)
),
}));
}
},
(v @ Value::Range { .. }, DataType::Text) => Some(Value::text(format_range_str(&v))),
(Value::Text(s), DataType::Inet) => match parse_inet_text(&s) {
Some((family, bits, addr)) => Some(Value::Inet { family, bits, addr }),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type inet: {s:?}"),
}));
}
},
(Value::Inet { family, bits, addr }, DataType::Cidr) => {
let full = if family == 6 { 128 } else { 32 };
let bits = if bits > full { full } else { bits };
let mut masked = addr;
for i in 0..16usize {
let bit_start = i * 8;
if bit_start >= usize::from(bits) {
masked[i] = 0;
} else if bit_start + 8 > usize::from(bits) {
let keep = usize::from(bits) - bit_start;
masked[i] &= 0xffu8 << (8 - keep);
}
}
Some(Value::Cidr {
family,
bits,
addr: masked,
})
}
(Value::Cidr { family, bits, addr }, DataType::Inet) => {
Some(Value::Inet { family, bits, addr })
}
(Value::Text(s), DataType::Cidr) => match parse_cidr_text(&s) {
Ok(Some((family, bits, addr))) => Some(Value::Cidr { family, bits, addr }),
Err(()) => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!(
"invalid cidr value: {s:?} DETAIL: Value has bits set to right of mask."
),
}));
}
Ok(None) => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type cidr: {s:?}"),
}));
}
},
(Value::Text(s), DataType::Interval) => match spg_sql::parser::parse_interval_text(&s) {
Some((months, days, micros)) => Some(Value::Interval {
months,
days,
micros,
}),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type interval: {s:?}"),
}));
}
},
(Value::Text(s), DataType::Macaddr) => match parse_macaddr_text(&s) {
Some(m) => Some(Value::Macaddr(m)),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type macaddr: {s:?}"),
}));
}
},
(Value::Text(s), DataType::PgLsn) => match parse_pg_lsn_text(&s) {
Some(l) => Some(Value::PgLsn(l)),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type pg_lsn: \"{s}\""),
}));
}
},
(Value::Text(s), DataType::Macaddr8) => match parse_macaddr8_text(&s) {
Some(m) => Some(Value::Macaddr8(m)),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type macaddr8: {s:?}"),
}));
}
},
(Value::BitString { nbits, bytes }, DataType::Bit(n)) => {
let want = if n == 0 { 1 } else { n };
if nbits != want {
return Err(EngineError::Unsupported(alloc::format!(
"bit string length {nbits} does not match type bit({want})"
)));
}
Some(Value::BitString { nbits, bytes })
}
(Value::BitString { nbits, bytes }, DataType::BitVarying(n)) => {
if n != 0 && nbits > n {
return Err(EngineError::Unsupported(alloc::format!(
"bit string too long for type bit varying({n})"
)));
}
Some(Value::BitString { nbits, bytes })
}
(Value::Text(s), bit_ty @ (DataType::Bit(_) | DataType::BitVarying(_))) => {
match parse_bit_string_text(&s) {
Some((nbits, bytes)) => {
match bit_ty {
DataType::Bit(n) => {
let want = if n == 0 { 1 } else { n };
if nbits != want {
return Err(EngineError::Unsupported(alloc::format!(
"bit string length {nbits} does not match type bit({want})"
)));
}
}
DataType::BitVarying(n) if n != 0 && nbits > n => {
return Err(EngineError::Unsupported(alloc::format!(
"bit string too long for type bit varying({n})"
)));
}
_ => {}
}
Some(Value::bit_string(nbits, bytes))
}
None => {
let bad = s.chars().find(|c| *c != '0' && *c != '1');
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: match bad {
Some(c) => {
alloc::format!("\"{c}\" is not a valid binary digit")
}
None => alloc::format!("invalid input syntax for BIT: {s:?}"),
},
}));
}
}
}
(Value::Text(s), DataType::Xml) => {
if !xml_content_is_well_formed(&s) {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid XML content: {s:?}"),
}));
}
Some(Value::xml(s))
}
(Value::BpChar(s), DataType::Char1) => {
Some(Value::Char1(s.as_bytes().first().copied().unwrap_or(0)))
}
(Value::BpChar(s), DataType::Xml) => {
let stripped = s.trim_end_matches(' ');
if !xml_content_is_well_formed(stripped) {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid XML content: {stripped:?}"),
}));
}
Some(Value::xml(alloc::string::String::from(stripped)))
}
(Value::Bytes(b), DataType::SmallInt | DataType::Int | DataType::BigInt) => {
let mut acc: i128 = 0;
for byte in b.iter() {
acc = acc.saturating_mul(256).saturating_add(i128::from(*byte));
}
let (fits, made) = match expected {
DataType::SmallInt => (
i16::try_from(acc).is_ok(),
i16::try_from(acc).map(Value::SmallInt).ok(),
),
DataType::Int => (
i32::try_from(acc).is_ok(),
i32::try_from(acc).map(Value::Int).ok(),
),
_ => (
i64::try_from(acc).is_ok(),
i64::try_from(acc).map(Value::BigInt).ok(),
),
};
if !fits {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("{} out of range", pg_type_name_for_error(expected)),
}));
}
made
}
(Value::Int(n), DataType::Char1) => Some(Value::Char1((n & 0xff) as u8)),
(Value::SmallInt(n), DataType::Char1) => Some(Value::Char1((n & 0xff) as u8)),
(Value::BigInt(n), DataType::Char1) => Some(Value::Char1((n & 0xff) as u8)),
(Value::Text(s), DataType::Char1) => {
let bytes = s.as_bytes();
if bytes.len() == 4
&& bytes[0] == b'\\'
&& bytes[1..].iter().all(|b| (b'0'..=b'7').contains(b))
{
let v = ((bytes[1] - b'0') << 6) | ((bytes[2] - b'0') << 3) | (bytes[3] - b'0');
Some(Value::Char1(v))
} else {
let b = s.bytes().next().unwrap_or(0);
Some(Value::Char1(b))
}
}
(Value::Inet { family, bits, addr }, DataType::Text) => {
let base = format_inet(family, bits, &addr);
Some(Value::text(if base.contains('/') {
base
} else {
alloc::format!("{base}/{bits}")
}))
}
(Value::Cidr { family, bits, addr }, DataType::Text) => {
Some(Value::text(format_inet(family, bits, &addr)))
}
(Value::Macaddr(m), DataType::Text) => Some(Value::text(format_macaddr(&m))),
(Value::Macaddr8(m), DataType::Text) => Some(Value::text(format_macaddr8(&m))),
(Value::PgLsn(l), DataType::Text) => Some(Value::text(format_pg_lsn(l))),
(Value::Macaddr(m), DataType::Macaddr8) => Some(Value::Macaddr8([
m[0], m[1], m[2], 0xff, 0xfe, m[3], m[4], m[5],
])),
(Value::BitString { nbits, bytes }, DataType::Text) => {
Some(Value::text(format_bit_string(nbits, &bytes)))
}
#[allow(clippy::cast_possible_truncation)]
(Value::BitString { nbits, bytes }, DataType::SmallInt) => {
Some(Value::SmallInt(bit_string_to_i64(nbits, &bytes) as i16))
}
#[allow(clippy::cast_possible_truncation)]
(Value::BitString { nbits, bytes }, DataType::Int) => {
Some(Value::Int(bit_string_to_i64(nbits, &bytes) as i32))
}
(Value::BitString { nbits, bytes }, DataType::BigInt) => {
Some(Value::BigInt(bit_string_to_i64(nbits, &bytes)))
}
(Value::Xml(s), DataType::Text) => Some(Value::text(s)),
(Value::Char1(b), DataType::Text) => Some(Value::text((b as char).to_string())),
(Value::Text(s), DataType::Point) => match parse_point(&s) {
Some(p) => Some(Value::Point(p)),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type point: {s:?}"),
}));
}
},
(Value::Text(s), DataType::Lseg) => match parse_lseg_text(&s) {
Some((p1, p2)) => Some(Value::Lseg(p1, p2)),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type lseg: {s:?}"),
}));
}
},
(Value::Text(s), DataType::PgBox) => match parse_box_text(&s) {
Some((ur, ll)) => Some(Value::PgBox(ur, ll)),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type box: {s:?}"),
}));
}
},
(Value::Text(s), DataType::Line) => match parse_line_text(&s) {
Some((a, b, c)) => Some(Value::Line { a, b, c }),
None => {
let zero_ab = s
.trim()
.strip_prefix('{')
.and_then(|x| x.strip_suffix('}'))
.map(|inner| inner.split(',').collect::<alloc::vec::Vec<_>>())
.is_some_and(|parts| {
parts.len() == 3
&& parts[0].trim().parse::<f64>() == Ok(0.0)
&& parts[1].trim().parse::<f64>() == Ok(0.0)
&& parts[2].trim().parse::<f64>().is_ok()
});
let detail = if zero_ab {
alloc::string::String::from(
"invalid line specification: A and B cannot both be zero",
)
} else {
alloc::format!("invalid input syntax for type line: {s:?}")
};
return Err(EngineError::Eval(EvalError::TypeMismatch { detail }));
}
},
(Value::Text(s), DataType::Circle) => match parse_circle_text(&s) {
Some((center, radius)) => Some(Value::Circle { center, radius }),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type circle: {s:?}"),
}));
}
},
(Value::Text(s), DataType::Path) => match parse_path_text(&s) {
Some((points, closed)) => Some(Value::Path { points, closed }),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type path: {s:?}"),
}));
}
},
(Value::PgBox(a, b), DataType::Polygon) => {
let (hx, hy) = (a.x.max(b.x), a.y.max(b.y));
let (lx, ly) = (a.x.min(b.x), a.y.min(b.y));
let p = |x: f64, y: f64| spg_storage::Point2D { x, y };
Some(Value::Polygon(alloc::vec![
p(lx, ly),
p(lx, hy),
p(hx, hy),
p(hx, ly),
]))
}
(Value::Text(s), DataType::Polygon) => match parse_polygon_text(&s) {
Some(points) => Some(Value::Polygon(points)),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type polygon: {s:?}"),
}));
}
},
(Value::Point(p), DataType::Text) => Some(Value::text(format_point(p))),
(Value::Lseg(p1, p2), DataType::Text) => Some(Value::text(format_lseg(p1, p2))),
(Value::PgBox(ur, ll), DataType::Text) => Some(Value::text(format_pg_box(ur, ll))),
(Value::Line { a, b, c }, DataType::Text) => Some(Value::text(format_line(a, b, c))),
(Value::Circle { center, radius }, DataType::Text) => {
Some(Value::text(format_circle(center, radius)))
}
(Value::Path { points, closed }, DataType::Text) => {
Some(Value::text(format_path(&points, closed)))
}
(Value::Polygon(points), DataType::Text) => Some(Value::text(format_polygon(&points))),
(ref rv @ Value::Range { kind: rk, .. }, DataType::Multirange(kind)) => {
if rk != kind {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!(
"cannot cast type {} to {}",
DataType::Range(rk),
DataType::Multirange(kind)
),
}));
}
crate::eval::binop::range_as_multirange(rv)
}
(Value::Text(s), DataType::Multirange(kind)) => match parse_multirange_str(&s, kind) {
Some(ranges) => Some(Value::Multirange {
kind,
ranges: crate::eval::binop::normalize_multirange_spans(kind, &ranges),
}),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for multirange type: {s:?}"),
}));
}
},
(Value::Multirange { ranges, .. }, DataType::Text) => {
Some(Value::text(format_multirange(&ranges)))
}
(Value::Text(s), DataType::Hstore) => match parse_hstore_str(&s) {
Some(pairs) => Some(Value::Hstore(pairs)),
None => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for type hstore: {s:?}"),
}));
}
},
(Value::Hstore(pairs), DataType::Text) => Some(Value::text(format_hstore_str(&pairs))),
(Value::Text(s), DataType::IntArray2D) => match parse_int_2d_literal(&s) {
Ok(m) => Some(Value::IntArray2D(m)),
Err(e) => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for INT[][]: {s:?}: {e}"),
}));
}
},
(Value::Text(s), DataType::BigIntArray2D) => match parse_bigint_2d_literal(&s) {
Ok(m) => Some(Value::BigIntArray2D(m)),
Err(e) => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for BIGINT[][]: {s:?}: {e}"),
}));
}
},
(Value::Text(s), DataType::TextArray2D) => match parse_text_2d_literal(&s) {
Ok(m) => Some(Value::TextArray2D(m)),
Err(e) => {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("invalid input syntax for TEXT[][]: {s:?}: {e}"),
}));
}
},
(Value::IntArray2D(rows), DataType::Text) => Some(Value::text(format_int_2d_text(&rows))),
(Value::BigIntArray2D(rows), DataType::Text) => {
Some(Value::text(format_bigint_2d_text(&rows)))
}
(Value::TextArray2D(rows), DataType::Text) => Some(Value::text(format_text_2d_text(&rows))),
(Value::Text(s), DataType::TextArray) => {
let arr = decode_text_array_literal(&s).map_err(|_| {
EngineError::Eval(EvalError::TypeMismatch {
detail: malformed_array_literal(&s),
})
})?;
Some(Value::TextArray(arr))
}
(Value::Text(s), DataType::IntArray) => {
let arr = decode_text_array_literal(&s).map_err(|_| {
EngineError::Eval(EvalError::TypeMismatch {
detail: malformed_array_literal(&s),
})
})?;
let mut out: Vec<Option<i32>> = Vec::with_capacity(arr.len());
for elem in arr {
match elem {
None => out.push(None),
Some(t) => {
let n: i32 = t.parse().map_err(|_| {
EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!(
"invalid input syntax for type integer: {t:?}"
),
})
})?;
out.push(Some(n));
}
}
}
Some(Value::IntArray(out))
}
(Value::Text(s), DataType::SmallIntArray) => Some(Value::SmallIntArray(
decode_array_elems(&s, DataType::SmallInt, col_name, position)?
.into_iter()
.map(|o| match o {
Some(Value::SmallInt(n)) => Some(n),
_ => None,
})
.collect(),
)),
(Value::Text(s), DataType::BoolArray) => {
if let Some(rows) = crate::eval::values::split_2d_rows(&s) {
let mut row_vals: Vec<Value<'static>> = Vec::with_capacity(rows.len());
for r in &rows {
let bools: Vec<Option<bool>> =
decode_array_elems(r, DataType::Bool, col_name, position)?
.into_iter()
.map(|o| match o {
Some(Value::Bool(b)) => Some(b),
_ => None,
})
.collect();
row_vals.push(Value::BoolArray(bools));
}
return crate::eval::values::build_2d_from_rows(&row_vals).ok_or_else(|| {
EngineError::Eval(EvalError::TypeMismatch {
detail: malformed_array_literal(&s),
})
});
}
Some(Value::BoolArray(
decode_array_elems(&s, DataType::Bool, col_name, position)?
.into_iter()
.map(|o| match o {
Some(Value::Bool(b)) => Some(b),
_ => None,
})
.collect(),
))
}
(Value::Text(s), DataType::FloatArray) => Some(Value::FloatArray(
decode_array_elems(&s, DataType::Float, col_name, position)?
.into_iter()
.map(|o| match o {
Some(Value::Float(f)) => Some(f),
_ => None,
})
.collect(),
)),
(Value::Text(s), DataType::NumericArray) => Some(Value::NumericArray(
decode_array_elems(
&s,
DataType::Numeric {
precision: 0,
scale: 0,
},
col_name,
position,
)?
.into_iter()
.map(|o| match o {
Some(Value::Numeric { scaled, scale, .. }) => Some((scaled, scale)),
_ => None,
})
.collect(),
)),
(Value::Text(s), DataType::DateArray) => Some(Value::DateArray(
decode_array_elems(&s, DataType::Date, col_name, position)?
.into_iter()
.map(|o| match o {
Some(Value::Date(d)) => Some(d),
_ => None,
})
.collect(),
)),
(Value::Text(s), DataType::UuidArray) => Some(Value::UuidArray(
decode_array_elems(&s, DataType::Uuid, col_name, position)?
.into_iter()
.map(|o| match o {
Some(Value::Uuid(u)) => Some(u),
_ => None,
})
.collect(),
)),
(Value::Text(s), DataType::BigIntArray | DataType::OidArray) => {
let arr = decode_text_array_literal(&s).map_err(|_| {
EngineError::Eval(EvalError::TypeMismatch {
detail: malformed_array_literal(&s),
})
})?;
let mut out: Vec<Option<i64>> = Vec::with_capacity(arr.len());
for elem in arr {
match elem {
None => out.push(None),
Some(t) => {
let n: i64 = t.parse().map_err(|_| {
EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!(
"invalid input syntax for type bigint: {t:?}"
),
})
})?;
out.push(Some(n));
}
}
}
Some(Value::BigIntArray(out))
}
(Value::TextArray(items), DataType::Text) => Some(Value::text(encode_text_array(&items))),
(Value::TextArray(items), DataType::BoolArray) if items.is_empty() => {
Some(Value::BoolArray(alloc::vec::Vec::new()))
}
(Value::TextArray(items), DataType::SmallIntArray) if items.is_empty() => {
Some(Value::SmallIntArray(alloc::vec::Vec::new()))
}
(Value::TextArray(items), DataType::IntArray) if items.is_empty() => {
Some(Value::IntArray(alloc::vec::Vec::new()))
}
(Value::TextArray(items), DataType::BigIntArray) if items.is_empty() => {
Some(Value::BigIntArray(alloc::vec::Vec::new()))
}
(Value::TextArray(items), DataType::FloatArray) if items.is_empty() => {
Some(Value::FloatArray(alloc::vec::Vec::new()))
}
(Value::TextArray(items), DataType::FloatArray) => {
let mut out = alloc::vec::Vec::with_capacity(items.len());
let mut ok = true;
for item in items {
match item {
None => out.push(None),
Some(s) => match s.trim().parse::<f64>() {
Ok(x) => out.push(Some(x)),
Err(_) => {
ok = false;
break;
}
},
}
}
if ok {
Some(Value::FloatArray(out))
} else {
None
}
}
(Value::FloatArray(items), DataType::FloatArray) => Some(Value::FloatArray(items)),
#[allow(clippy::cast_precision_loss)]
(Value::IntArray(items), DataType::FloatArray) => Some(Value::FloatArray(
items.into_iter().map(|o| o.map(|n| f64::from(n))).collect(),
)),
#[allow(clippy::cast_precision_loss)]
(Value::BigIntArray(items), DataType::FloatArray) => Some(Value::FloatArray(
items.into_iter().map(|o| o.map(|n| n as f64)).collect(),
)),
#[allow(clippy::cast_precision_loss)]
(Value::NumericArray(items), DataType::FloatArray) => Some(Value::FloatArray(
items
.into_iter()
.map(|o| {
o.map(|(scaled, scale)| {
crate::eval::format_numeric(scaled, scale)
.parse()
.unwrap_or(f64::NAN)
})
})
.collect(),
)),
(Value::IntArray(items), DataType::BigIntArray) => Some(Value::BigIntArray(
items.into_iter().map(|o| o.map(i64::from)).collect(),
)),
(Value::BigIntArray(items), DataType::IntArray) => {
let mut out = alloc::vec::Vec::with_capacity(items.len());
let mut ok = true;
for o in items {
match o {
None => out.push(None),
Some(n) => match i32::try_from(n) {
Ok(v) => out.push(Some(v)),
Err(_) => {
ok = false;
break;
}
},
}
}
if ok { Some(Value::IntArray(out)) } else { None }
}
(Value::IntArray(items), DataType::NumericArray) => Some(Value::NumericArray(
items
.into_iter()
.map(|o| o.map(|n| (i128::from(n), 0_u16)))
.collect(),
)),
(Value::BigIntArray(items), DataType::NumericArray) => Some(Value::NumericArray(
items
.into_iter()
.map(|o| o.map(|n| (i128::from(n), 0_u16)))
.collect(),
)),
(Value::FloatArray(items), DataType::NumericArray) => {
let mut out = alloc::vec::Vec::with_capacity(items.len());
let mut ok = true;
for o in items {
match o {
None => out.push(None),
Some(x) => match parse_numeric_text(&alloc::format!("{x}")) {
Some((mantissa, scale)) => out.push(Some((mantissa, scale))),
None => {
ok = false;
break;
}
},
}
}
if ok {
Some(Value::NumericArray(out))
} else {
None
}
}
(Value::NumericArray(items), DataType::IntArray) => {
let mut out = alloc::vec::Vec::with_capacity(items.len());
let mut ok = true;
for o in items {
match o {
None => out.push(None),
Some((scaled, scale)) => {
match i32::try_from(numeric_round_to_integer(scaled, scale)) {
Ok(v) => out.push(Some(v)),
Err(_) => {
ok = false;
break;
}
}
}
}
}
if ok { Some(Value::IntArray(out)) } else { None }
}
(Value::NumericArray(items), DataType::BigIntArray) => {
let mut out = alloc::vec::Vec::with_capacity(items.len());
let mut ok = true;
for o in items {
match o {
None => out.push(None),
Some((scaled, scale)) => {
match i64::try_from(numeric_round_to_integer(scaled, scale)) {
Ok(v) => out.push(Some(v)),
Err(_) => {
ok = false;
break;
}
}
}
}
}
if ok {
Some(Value::BigIntArray(out))
} else {
None
}
}
#[allow(clippy::cast_possible_truncation)]
(Value::FloatArray(items), DataType::IntArray) => {
let mut out = alloc::vec::Vec::with_capacity(items.len());
let mut ok = true;
for o in items {
match o {
None => out.push(None),
Some(x) if x.is_finite() => {
let r = crate::eval::math::f64_round_half_even(x);
if r >= f64::from(i32::MIN) && r <= f64::from(i32::MAX) {
out.push(Some(r as i32));
} else {
ok = false;
break;
}
}
Some(_) => {
ok = false;
break;
}
}
}
if ok { Some(Value::IntArray(out)) } else { None }
}
#[allow(clippy::cast_possible_truncation)]
(Value::FloatArray(items), DataType::BigIntArray) => {
let mut out = alloc::vec::Vec::with_capacity(items.len());
let mut ok = true;
for o in items {
match o {
None => out.push(None),
Some(x) if x.is_finite() => {
out.push(Some(crate::eval::math::f64_round_half_even(x) as i64));
}
Some(_) => {
ok = false;
break;
}
}
}
if ok {
Some(Value::BigIntArray(out))
} else {
None
}
}
(Value::TextArray(items), DataType::NumericArray) if items.is_empty() => {
Some(Value::NumericArray(alloc::vec::Vec::new()))
}
(Value::TextArray(items), DataType::DateArray) if items.is_empty() => {
Some(Value::DateArray(alloc::vec::Vec::new()))
}
(Value::TextArray(items), DataType::TimestampArray) if items.is_empty() => {
Some(Value::TimestampArray(alloc::vec::Vec::new()))
}
(Value::TextArray(items), DataType::TimestamptzArray) if items.is_empty() => {
Some(Value::TimestamptzArray(alloc::vec::Vec::new()))
}
(Value::TextArray(items), DataType::UuidArray) if items.is_empty() => {
Some(Value::UuidArray(alloc::vec::Vec::new()))
}
(Value::TextArray(items), DataType::JsonArray) if items.is_empty() => {
Some(Value::JsonArray(alloc::vec::Vec::new()))
}
(Value::TextArray(items), DataType::JsonbArray) if items.is_empty() => {
Some(Value::JsonbArray(alloc::vec::Vec::new()))
}
(Value::TextArray(items), DataType::BytesArray) if items.is_empty() => {
Some(Value::BytesArray(alloc::vec::Vec::new()))
}
(Value::TextArray(items), DataType::IntervalArray) if items.is_empty() => {
Some(Value::IntervalArray(alloc::vec::Vec::new()))
}
(
Value::TextArray(items),
dt @ (DataType::BoolArray
| DataType::NumericArray
| DataType::DateArray
| DataType::TimestampArray
| DataType::TimestamptzArray
| DataType::IntervalArray
| DataType::UuidArray),
) => coerce_text_array_to(items, dt, col_name)?,
(
Value::Text(s),
dt @ (DataType::TimestampArray | DataType::TimestamptzArray | DataType::IntervalArray),
) => {
let items = decode_text_array_literal(&s).map_err(|_| {
EngineError::Eval(EvalError::TypeMismatch {
detail: malformed_array_literal(&s),
})
})?;
coerce_text_array_to(items, dt, col_name)?
}
(Value::TextArray(items), DataType::MoneyArray) if items.is_empty() => {
Some(Value::MoneyArray(alloc::vec::Vec::new()))
}
(Value::IntArray(items), DataType::SmallIntArray) => {
let mut out = alloc::vec::Vec::with_capacity(items.len());
let mut ok = true;
for item in items {
match item {
None => out.push(None),
Some(n) => match i16::try_from(n) {
Ok(x) => out.push(Some(x)),
Err(_) => {
ok = false;
break;
}
},
}
}
if ok {
Some(Value::SmallIntArray(out))
} else {
None
}
}
(Value::Text(s), DataType::Vector { dim, encoding }) => {
let parsed = eval::parse_vector_text(&s).ok_or_else(|| {
EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("cannot parse {s:?} as VECTOR"),
})
})?;
if parsed.len() != dim as usize {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!(
"VECTOR({dim}) column `{col_name}` rejects literal of length {}",
parsed.len()
),
}));
}
Some(match encoding {
VecEncoding::F32 => Value::vector(parsed),
VecEncoding::Sq8 => Value::Sq8Vector(spg_storage::quantize::quantize(&parsed)),
VecEncoding::F16 => {
Value::HalfVector(spg_storage::halfvec::HalfVector::from_f32_slice(&parsed))
}
})
}
(Value::Text(s), DataType::TsVector) => {
let lexs = eval::decode_tsvector_external(&s).map_err(|e| {
EngineError::Eval(EvalError::TypeMismatch {
detail: alloc::format!("cannot parse {s:?} as TSVECTOR: {e}"),
})
})?;
Some(Value::TsVector(lexs))
}
(Value::Text(s), DataType::Timestamp | DataType::Timestamptz) => {
let t = eval::parse_timestamp_literal(&s)
.ok_or_else(|| datetime_parse_error("timestamp", &s))?;
Some(Value::Timestamp(t))
}
(Value::Date(i32::MAX), DataType::Timestamp | DataType::Timestamptz) => {
Some(Value::Timestamp(i64::MAX))
}
(Value::Date(i32::MIN), DataType::Timestamp | DataType::Timestamptz) => {
Some(Value::Timestamp(i64::MIN))
}
(Value::Date(d), DataType::Timestamp | DataType::Timestamptz) => {
Some(Value::Timestamp(i64::from(d) * 86_400_000_000))
}
(Value::Timestamp(t), DataType::Timestamptz) => Some(Value::Timestamp(t)),
(Value::Timestamp(t), DataType::Date) => {
let days = t.div_euclid(86_400_000_000);
i32::try_from(days).ok().map(Value::Date)
}
(Value::Timestamp(t), DataType::Time) => Some(Value::Time(t.rem_euclid(86_400_000_000))),
(
Value::NumericBig(b),
DataType::Numeric {
precision: 0,
scale: 0,
},
) => Some(Value::NumericBig(b)),
(
Value::Numeric {
scaled,
scale: src_scale,
..
},
DataType::Numeric { precision, scale },
) => {
if precision == 0 && scale == 0 {
Some(Value::Numeric {
scaled,
scale: src_scale,
kind: spg_storage::NumericKind::Finite,
})
} else {
Some(numeric_rescale(
scaled, src_scale, precision, scale, col_name,
)?)
}
}
(Value::NumericBig(b), DataType::Numeric { precision, scale }) => {
if precision == 0 && scale == 0 {
Some(Value::NumericBig(b))
} else {
#[allow(clippy::cast_sign_loss)]
let rounded = if scale < 0 {
b.round_to(0)
} else {
b.round_to(scale as u16)
};
let out = crate::eval::binop::bignum_to_value(rounded);
crate::numeric::check_precision_text(&out, precision, scale, col_name)?;
Some(out)
}
}
#[allow(clippy::cast_precision_loss)]
(Value::Numeric { scaled, scale, .. }, DataType::Float) => {
let text = crate::eval::format_numeric(scaled, scale);
let x: f64 = text.parse().unwrap_or(f64::NAN);
if x == 0.0 && scaled != 0 {
return Err(float_out_of_range(
&crate::eval::format_numeric(scaled, scale),
"double precision",
));
}
Some(Value::Float(x))
}
(Value::NumericBig(b), DataType::Real) => {
let text = b.to_decimal_str();
let x: f32 = text.parse().map_err(|_| real_out_of_range(&text))?;
if !x.is_finite() || (x == 0.0 && float_text_is_nonzero(&text)) {
return Err(real_out_of_range(&text));
}
Some(Value::Real(x))
}
(Value::NumericBig(b), DataType::Float) => {
let text = b.to_decimal_str();
let x: f64 = text
.parse()
.map_err(|_| float_out_of_range(&text, "double precision"))?;
if !x.is_finite() || (x == 0.0 && float_text_is_nonzero(&text)) {
return Err(float_out_of_range(&text, "double precision"));
}
Some(Value::Float(x))
}
(Value::Float(x), DataType::Int) => {
let r = crate::eval::math::f64_round_half_even(x);
if !r.is_finite() || !(-2_147_483_648.0..=2_147_483_647.0).contains(&r) {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: "integer out of range".into(),
}));
}
#[allow(clippy::cast_possible_truncation)]
Some(Value::Int(r as i32))
}
(Value::Float(x), DataType::BigInt) => {
let r = crate::eval::math::f64_round_half_even(x);
if !r.is_finite()
|| !(-9.223_372_036_854_776e18..=9.223_372_036_854_776e18).contains(&r)
{
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: "bigint out of range".into(),
}));
}
#[allow(clippy::cast_possible_truncation)]
Some(Value::BigInt(r as i64))
}
(Value::Float(x), DataType::SmallInt) => {
let r = crate::eval::math::f64_round_half_even(x);
if !r.is_finite() || !(-32768.0..=32767.0).contains(&r) {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: "smallint out of range".into(),
}));
}
#[allow(clippy::cast_possible_truncation)]
Some(Value::SmallInt(r as i16))
}
(Value::Real(x), DataType::Int) => {
let r = crate::eval::math::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(EngineError::Eval(EvalError::TypeMismatch {
detail: "integer out of range".into(),
}));
}
#[allow(clippy::cast_possible_truncation)]
Some(Value::Int(r as i32))
}
(Value::Real(x), DataType::BigInt) => {
let r = crate::eval::math::f64_round_half_even(f64::from(x));
if !r.is_finite()
|| !(-9.223_372_036_854_776e18..=9.223_372_036_854_776e18).contains(&r)
{
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: "bigint out of range".into(),
}));
}
#[allow(clippy::cast_possible_truncation)]
Some(Value::BigInt(r as i64))
}
(Value::Real(x), DataType::SmallInt) => {
let r = crate::eval::math::f64_round_half_even(f64::from(x));
if !r.is_finite() || !(-32768.0..=32767.0).contains(&r) {
return Err(EngineError::Eval(EvalError::TypeMismatch {
detail: "smallint out of range".into(),
}));
}
#[allow(clippy::cast_possible_truncation)]
Some(Value::SmallInt(r as i16))
}
(Value::Numeric { scaled, scale, .. }, DataType::Int) => {
let rounded = numeric_round_to_integer(scaled, scale);
i32::try_from(rounded).ok().map(Value::Int)
}
(Value::Numeric { scaled, scale, .. }, DataType::BigInt) => {
let rounded = numeric_round_to_integer(scaled, scale);
i64::try_from(rounded).ok().map(Value::BigInt)
}
(Value::Numeric { scaled, scale, .. }, DataType::SmallInt) => {
let rounded = numeric_round_to_integer(scaled, scale);
i16::try_from(rounded).ok().map(Value::SmallInt)
}
(Value::Text(s), DataType::Name) => {
let mut cut = s.into_owned();
if cut.len() > 63 {
let mut idx = 63;
while !cut.is_char_boundary(idx) {
idx -= 1;
}
cut.truncate(idx);
}
Some(Value::text(cut))
}
(Value::Text(s), DataType::Varchar(max)) => {
if max == 0 || u32::try_from(s.chars().count()).unwrap_or(u32::MAX) <= max {
Some(Value::text(s))
} else {
let excess_all_blanks = s.chars().skip(max as usize).all(|c| c == ' ');
if excess_all_blanks {
Some(Value::text(
s.chars()
.take(max as usize)
.collect::<alloc::string::String>(),
))
} else {
return Err(EngineError::Unsupported(alloc::format!(
"value too long for type character varying({max})"
)));
}
}
}
(
Value::Vector(v),
DataType::Vector {
dim,
encoding: VecEncoding::Sq8,
},
) if v.len() == dim as usize => Some(Value::Sq8Vector(spg_storage::quantize::quantize(&v))),
(
Value::Vector(v),
DataType::Vector {
dim,
encoding: VecEncoding::F16,
},
) if v.len() == dim as usize => Some(Value::HalfVector(
spg_storage::halfvec::HalfVector::from_f32_slice(&v),
)),
(Value::Text(s), DataType::Char(size)) => {
if size == 0 {
return Ok(Value::BpChar(alloc::borrow::Cow::Owned(
s.trim_end_matches(' ').to_string(),
)));
}
let len = u32::try_from(s.chars().count()).unwrap_or(u32::MAX);
let body = if len > size {
let trimmed = s.trim_end_matches(' ');
let tlen = u32::try_from(trimmed.chars().count()).unwrap_or(u32::MAX);
if tlen > size {
return Err(EngineError::Unsupported(alloc::format!(
"value too long for type character({size})"
)));
}
trimmed.to_string()
} else {
s.into_owned()
};
let need = (size as usize) - body.chars().count();
let mut padded = body;
padded.reserve(need);
for _ in 0..need {
padded.push(' ');
}
Some(Value::BpChar(alloc::borrow::Cow::Owned(padded)))
}
_ => None,
};
coerced.ok_or_else(|| {
EngineError::Storage(StorageError::TypeMismatch {
column: col_name.into(),
expected,
actual,
position,
})
})
}
pub(crate) fn big_literal_to_value(s: &str) -> Value<'static> {
let b = spg_storage::bignum::BigNumeric::from_decimal_str(s).expect("lexer-validated decimal");
match b.to_i128() {
Some(scaled) => Value::Numeric {
scaled,
scale: b.scale(),
kind: spg_storage::NumericKind::Finite,
},
None => Value::NumericBig(alloc::boxed::Box::new(b)),
}
}
pub(crate) fn types_unify(a: DataType, b: DataType) -> bool {
fn category(t: DataType) -> Option<u8> {
Some(match t {
DataType::SmallInt
| DataType::Int
| DataType::BigInt
| DataType::Numeric { .. }
| DataType::Real
| DataType::Float => 1,
DataType::Text | DataType::Varchar(_) | DataType::Char(_) => 2,
DataType::Date | DataType::Timestamp | DataType::Timestamptz => 3,
_ => return None,
})
}
if a == b {
return true;
}
match (category(a), category(b)) {
(Some(x), Some(y)) => x == y,
_ => false,
}
}
pub(crate) fn pg_type_name_for_error_opt(t: Option<DataType>) -> alloc::string::String {
match t {
Some(t) => pg_type_name_for_error(t),
None => alloc::string::String::from("unknown"),
}
}
pub(crate) fn pg_type_name_for_error(t: DataType) -> alloc::string::String {
use spg_storage::DataType as D;
let elem = match t {
D::TextArray => Some(D::Text),
D::IntArray => Some(D::Int),
D::BigIntArray => Some(D::BigInt),
D::SmallIntArray => Some(D::SmallInt),
D::FloatArray => Some(D::Float),
D::NumericArray => Some(D::Numeric {
precision: 0,
scale: 0,
}),
D::BoolArray => Some(D::Bool),
D::DateArray => Some(D::Date),
D::TimestampArray => Some(D::Timestamp),
D::TimestamptzArray => Some(D::Timestamptz),
D::IntervalArray => Some(D::Interval),
D::UuidArray => Some(D::Uuid),
D::JsonArray | D::JsonbArray => Some(D::Jsonb),
D::BytesArray => Some(D::Bytes),
D::MoneyArray => Some(D::Money),
_ => None,
};
match elem {
Some(e) => alloc::format!("{}[]", crate::system_catalog::pg_data_type_text(e)),
None => crate::system_catalog::pg_data_type_text(t),
}
}