use alloc::borrow::Cow;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use spg_sql::ast::{BinOp, CastTarget, ColumnName, Expr, Literal};
use spg_storage::{ColumnSchema, Row, Value};
pub(crate) mod binop;
mod cast;
pub mod compiled;
mod datetime;
mod encoding;
mod encodings;
mod format;
pub(crate) mod functions;
mod inet;
pub(crate) mod math;
mod regexp;
mod resolve;
mod strings;
mod textsearch;
pub(crate) mod values;
pub use crate::conversions::format_money_array;
pub(crate) use binop::{
add_interval_to_micros, and_3vl, apply_binary, apply_binary_by_ref, apply_binary_interval,
};
use binop::{apply_binary_in, apply_unary, compare, pow10_i128};
pub use cast::{cast_to_vector, cast_value, parse_vector_text};
pub(crate) use compiled::{
CompiledExpr, compile_column_pos, compile_expr, eval_compiled, eval_compiled_ref,
fully_compilable,
};
use datetime::{
age, date_format_mysql, date_part, date_trunc, extract_field, from_unixtime, unix_timestamp_of,
};
use encoding::{decode_text, encode_text};
pub use format::{
days_from_civil, format_bigint_array, format_bool_array, format_bytea_array, format_bytea_hex,
format_date, format_date_array, format_float, format_float_array, format_int_array,
format_interval, format_interval_array, format_money, format_numeric, format_numeric_array,
format_numeric_kind, format_real, format_smallint_array, format_text_array, format_time,
format_timestamp, format_timestamp_array, format_timestamptz, format_timestamptz_at,
format_timetz, format_uuid_array, parse_date_literal, parse_timestamp_literal,
};
pub use format::{
DateOrder, DateStyleKind, IntervalStyleKind, RenderStyle, format_date_array_styled,
format_date_styled, format_float_array_styled, format_float_styled,
format_interval_array_styled, format_interval_styled, format_real_styled,
format_timestamp_array_styled, format_timestamp_styled, format_timestamptz_styled,
format_timestamptz_tz, parse_date_literal_ordered, parse_timestamp_literal_ordered,
parse_timestamp_literal_tz_ordered,
};
use functions::apply_function;
use inet::{inet_host, inet_masklen, inet_network, inet_op_bool_result};
pub(crate) use math::{f64_ceil, f64_floor, f64_sqrt};
use math::{
f64_exp, f64_ln, f64_powi, f64_round_half_away, f64_trunc, prng_next_f64, prng_next_u64,
};
pub(crate) use regexp::{
CompiledRe, compile_re, compiled_is_match, regex_is_match, regexp_matches_rows,
};
use regexp::{regexp_matches, regexp_replace, regexp_split_to_array};
use resolve::{
collation_fold_for_compare, compare_is_case_insensitive, composite_eq, eval_expr_cow,
is_owned_compare_value, resolve_column, resolve_column_borrowed, text_prefix_chars,
};
pub(crate) use resolve::{
column_at, column_collation, find_column_pos, is_binary_coerced, locate_column,
};
use strings::{
TrimSide, format_string, pg_quote_ident, pg_quote_literal, pg_typeof_name, string_left_right,
string_pad, string_trim, to_char, value_to_format_text,
};
pub use textsearch::{
decode_tsquery_external, decode_tsvector_external, format_tsquery, format_tsvector,
};
use textsearch::{
fts_phraseto_tsquery, fts_plainto_tsquery, fts_setweight, fts_to_tsquery, fts_to_tsvector,
fts_ts_headline, fts_ts_rank, fts_ts_rank_cd, fts_ts_rewrite, fts_tsquery_bool,
fts_websearch_to_tsquery, ts_match, tsvector_concat,
};
pub use values::gen_random_uuid_bytes;
pub(crate) fn datetime_resolve_zone_offset(z: &str) -> Option<i64> {
datetime::resolve_zone_offset(z)
}
pub use values::value_to_text;
pub use values::value_to_text_styled;
pub use values::value_to_text_with_fsp;
use values::{
array_2d_dims, array_element_at, array_len, array_rebuild, value_cmp_for_min_max, value_to_f64,
values_equal_for_nullif,
};
#[derive(Clone)]
#[allow(missing_debug_implementations)] pub struct EvalContext<'a> {
pub columns: &'a [ColumnSchema],
pub table_alias: Option<&'a str>,
pub params: &'a [Value<'static>],
pub default_text_search_config: Option<&'a str>,
pub sequence_resolver: Option<&'a SequenceResolver<'a>>,
pub catalog: Option<&'a spg_storage::Catalog>,
pub mysql_dialect: bool,
pub session_gucs: Option<&'a alloc::collections::BTreeMap<String, String>>,
pub users: Option<&'a crate::users::UserStore>,
pub fn_depth: u16,
pub engine: Option<&'a crate::Engine>,
pub sample_rng: Option<&'a core::cell::Cell<Option<u64>>>,
pub recursion_base: core::cell::Cell<usize>,
pub render_style: crate::eval::format::RenderStyle,
pub tz_offset_fn: Option<crate::TzOffsetFn>,
pub tz_localize_fn: Option<crate::TzLocalizeFn>,
pub tz_abbrev_fn: Option<crate::TzAbbrevFn>,
pub salt_fn: Option<crate::SaltFn>,
pub backend_pid_fn: Option<crate::BackendPidFn>,
pub wal_lsn_fn: Option<crate::WalLsnFn>,
pub backend_signal_fn: Option<crate::BackendSignalFn>,
pub clock: Option<crate::ClockFn>,
pub xact: Option<XactView<'a>>,
pub assigned_xid: core::cell::Cell<Option<u64>>,
}
#[derive(Clone, Copy, Debug)]
pub struct XactView<'a> {
pub current: Option<u64>,
pub active: &'a alloc::collections::BTreeSet<u64>,
pub aborted: &'a alloc::collections::BTreeSet<u64>,
}
pub type SequenceResolver<'a> = dyn Fn(SequenceOp) -> Result<i64, EvalError> + 'a;
#[derive(Debug, Clone)]
pub enum SequenceOp {
Next(String),
Curr(String),
Set {
name: String,
value: i64,
is_called: bool,
},
}
impl<'a> EvalContext<'a> {
pub const fn new(columns: &'a [ColumnSchema], table_alias: Option<&'a str>) -> Self {
Self {
columns,
table_alias,
params: &[],
default_text_search_config: None,
sequence_resolver: None,
catalog: None,
mysql_dialect: false,
session_gucs: None,
users: None,
fn_depth: 0,
engine: None,
sample_rng: None,
recursion_base: core::cell::Cell::new(0),
render_style: crate::eval::format::RenderStyle {
date_style: crate::eval::format::DateStyleKind::Iso,
date_order: crate::eval::format::DateOrder::Mdy,
interval_style: crate::eval::format::IntervalStyleKind::Postgres,
extra_float_digits: 1,
bytea_escape: false,
mysql: false,
},
tz_offset_fn: None,
tz_localize_fn: None,
tz_abbrev_fn: None,
salt_fn: None,
backend_pid_fn: None,
wal_lsn_fn: None,
backend_signal_fn: None,
clock: None,
xact: None,
assigned_xid: core::cell::Cell::new(None),
}
}
#[must_use]
pub const fn with_render_style(mut self, style: crate::eval::format::RenderStyle) -> Self {
self.render_style = style;
self
}
#[must_use]
pub const fn with_backend_signal_fn(mut self, f: Option<crate::BackendSignalFn>) -> Self {
self.backend_signal_fn = f;
self
}
#[must_use]
pub const fn with_wal_lsn_fn(mut self, f: Option<crate::WalLsnFn>) -> Self {
self.wal_lsn_fn = f;
self
}
#[must_use]
pub const fn with_backend_pid_fn(mut self, f: Option<crate::BackendPidFn>) -> Self {
self.backend_pid_fn = f;
self
}
#[must_use]
pub const fn with_tz_fns(
mut self,
offset: Option<crate::TzOffsetFn>,
localize: Option<crate::TzLocalizeFn>,
abbrev: Option<crate::TzAbbrevFn>,
) -> Self {
self.tz_offset_fn = offset;
self.tz_localize_fn = localize;
self.tz_abbrev_fn = abbrev;
self
}
#[must_use]
pub fn zone_offset_at(&self, zone: &str, utc_micros: i64) -> Option<i64> {
if let Some(off) = datetime::resolve_zone_offset(zone) {
return Some(off);
}
self.tz_offset_fn.and_then(|f| f(zone, utc_micros))
}
#[must_use]
pub fn session_tz_offset_at(&self, utc_micros: i64) -> i64 {
let Some(zone) = self.session_gucs.and_then(|g| g.get("timezone")) else {
return 0;
};
self.zone_offset_at(zone, utc_micros).unwrap_or(0)
}
#[must_use]
pub fn session_tz_abbrev_at(&self, utc_micros: i64) -> Option<alloc::string::String> {
let zone = self.session_gucs.and_then(|g| g.get("timezone"))?;
if datetime::resolve_zone_offset(zone).is_some()
|| zone.eq_ignore_ascii_case("utc")
|| zone.eq_ignore_ascii_case("gmt")
{
return None;
}
self.tz_abbrev_fn.and_then(|f| f(zone, utc_micros))
}
#[must_use]
pub fn zone_local_to_utc(&self, zone: &str, local_micros: i64) -> Option<i64> {
zone_local_to_utc_with(zone, local_micros, self.tz_localize_fn)
}
#[must_use]
pub const fn with_salt_fn(mut self, f: Option<crate::SaltFn>) -> Self {
self.salt_fn = f;
self
}
#[must_use]
pub const fn with_clock(mut self, f: Option<crate::ClockFn>) -> Self {
self.clock = f;
self
}
#[must_use]
pub const fn with_sample_rng(mut self, cell: &'a core::cell::Cell<Option<u64>>) -> Self {
self.sample_rng = Some(cell);
self
}
#[must_use]
pub const fn with_engine(mut self, engine: &'a crate::Engine) -> Self {
self.mysql_dialect = engine.backslash_escapes;
self.render_style.mysql = engine.backslash_escapes;
self.engine = Some(engine);
self
}
pub const fn with_users(mut self, users: &'a crate::users::UserStore) -> Self {
self.users = Some(users);
self
}
#[must_use]
pub(crate) fn with_session<'b: 'a>(mut self, s: &'b DmlSession) -> Self {
self.session_gucs = Some(&s.gucs);
self.users = Some(&s.users);
self.render_style = s.render_style;
self.tz_offset_fn = s.tz_offset_fn;
self.tz_localize_fn = s.tz_localize_fn;
self.tz_abbrev_fn = s.tz_abbrev_fn;
self
}
pub const fn with_session_gucs(
mut self,
gucs: &'a alloc::collections::BTreeMap<String, String>,
) -> Self {
self.session_gucs = Some(gucs);
self
}
#[must_use]
pub const fn with_catalog(mut self, catalog: &'a spg_storage::Catalog) -> Self {
self.catalog = Some(catalog);
self
}
#[must_use]
pub fn session_tz_offset(&self) -> i64 {
self.session_gucs
.and_then(|g| g.get("timezone"))
.and_then(|z| datetime::resolve_zone_offset(z))
.unwrap_or(0)
}
#[must_use]
pub const fn with_xact(mut self, xact: XactView<'a>) -> Self {
self.xact = Some(xact);
self
}
#[must_use]
pub const fn with_sequence_resolver(mut self, resolver: &'a SequenceResolver<'a>) -> Self {
self.sequence_resolver = Some(resolver);
self
}
#[must_use]
pub const fn with_params(mut self, params: &'a [Value<'static>]) -> Self {
self.params = params;
self
}
#[must_use]
pub const fn with_default_text_search_config(mut self, cfg: Option<&'a str>) -> Self {
self.default_text_search_config = cfg;
self
}
}
pub(crate) fn parse_timestamp_literal_tz_ordered_pub(
s: &str,
order: DateOrder,
) -> Option<(i64, bool)> {
format::parse_timestamp_literal_tz_ordered(s, order)
}
#[must_use]
pub(crate) fn resolve_zone_offset_pub(zone: &str) -> Option<i64> {
datetime::resolve_zone_offset(zone)
}
#[must_use]
pub(crate) fn zone_local_to_utc_with(
zone: &str,
local_micros: i64,
localize: Option<crate::TzLocalizeFn>,
) -> Option<i64> {
if let Some(off) = datetime::resolve_zone_offset(zone) {
return Some(local_micros - off);
}
localize.and_then(|f| f(zone, local_micros))
}
#[derive(Debug, Clone)]
pub(crate) struct SessionCoercion {
pub zone: Option<alloc::string::String>,
pub localize: Option<crate::TzLocalizeFn>,
pub order: DateOrder,
}
impl SessionCoercion {
#[must_use]
pub(crate) fn wall_to_utc(&self, wall: i64) -> Option<i64> {
let zone = self.zone.as_ref()?;
zone_local_to_utc_with(zone, wall, self.localize)
}
#[must_use]
pub(crate) fn from_ctx(ctx: &EvalContext<'_>) -> Option<Self> {
let zone = ctx
.session_gucs
.and_then(|g| g.get("timezone"))
.filter(|z| !z.eq_ignore_ascii_case("utc") && !z.eq_ignore_ascii_case("gmt"))
.cloned();
let order = ctx.render_style.date_order;
if zone.is_none() && order == DateOrder::Mdy {
return None;
}
Some(Self {
zone,
localize: ctx.tz_localize_fn,
order,
})
}
}
pub(crate) struct DmlSession {
pub gucs: alloc::collections::BTreeMap<String, String>,
pub users: crate::users::UserStore,
pub render_style: RenderStyle,
pub tz_offset_fn: Option<crate::TzOffsetFn>,
pub tz_localize_fn: Option<crate::TzLocalizeFn>,
pub tz_abbrev_fn: Option<crate::TzAbbrevFn>,
}
#[must_use]
pub(crate) fn session_read_temporal_text(
v: Value<'static>,
target: spg_storage::DataType,
coercion: Option<&SessionCoercion>,
) -> Value<'static> {
use spg_storage::DataType as D;
let Some(c) = coercion else { return v };
if c.order == DateOrder::Mdy {
return v;
}
let Value::Text(s) = &v else { return v };
match target {
D::Date => format::parse_date_literal_ordered(s, c.order).map_or(v, Value::Date),
D::Timestamp | D::Timestamptz => format::parse_timestamp_literal_tz_ordered(s, c.order)
.map_or(v, |(t, _)| Value::Timestamp(t)),
_ => v,
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum EvalError {
ColumnNotFound {
name: String,
},
UnknownQualifier {
qualifier: String,
},
DivisionByZero,
TypeMismatch {
detail: String,
},
PlaceholderOutOfRange {
n: u16,
bound: u16,
},
StackDepthExceeded,
}
impl core::fmt::Display for EvalError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::ColumnNotFound { name } => write!(f, "column \"{name}\" does not exist"),
Self::UnknownQualifier { qualifier } => {
write!(f, "missing FROM-clause entry for table \"{qualifier}\"")
}
Self::DivisionByZero => f.write_str("division by zero"),
Self::TypeMismatch { detail } => write!(f, "type mismatch: {detail}"),
Self::PlaceholderOutOfRange { n, bound } => write!(
f,
"parameter ${n} referenced but only {bound} bound by client"
),
Self::StackDepthExceeded => {
f.write_str("stack depth limit exceeded (expression nested too deeply)")
}
}
}
}
const MAX_EVAL_STACK_BYTES: usize = 768 * 1024;
#[inline(never)]
fn eval_stack_ptr() -> usize {
let probe = 0u8;
core::ptr::addr_of!(probe) as usize
}
fn apply_domain_constraints<'a>(
v: Value<'a>,
dom: &spg_storage::DomainDef,
name: &str,
cat: &spg_storage::Catalog,
) -> Result<Value<'a>, EvalError> {
if matches!(v, Value::Null) {
let mut cur = Some(dom);
while let Some(d) = cur {
if !d.nullable {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("domain {name} does not allow null values"),
});
}
cur = d
.base_domain
.as_ref()
.and_then(|p| cat.domain_types().get(p.as_str()));
}
return Ok(v);
}
let mut chain: alloc::vec::Vec<&spg_storage::DomainDef> = alloc::vec![dom];
let mut cur = dom;
while let Some(parent) = cur
.base_domain
.as_ref()
.and_then(|p| cat.domain_types().get(p.as_str()))
{
if chain.iter().any(|d| core::ptr::eq(*d, parent)) {
break;
}
chain.push(parent);
cur = parent;
}
chain.reverse();
for owner in chain {
apply_domain_checks_of(&v, owner, name)?;
}
Ok(v)
}
fn apply_domain_checks_of(
v: &Value<'_>,
dom: &spg_storage::DomainDef,
target: &str,
) -> Result<(), EvalError> {
let name = target;
for chk in &dom.checks {
let src = &chk.expr;
let owner = chk.name.as_str();
let expr = spg_sql::parser::parse_expression(src).map_err(|e| EvalError::TypeMismatch {
detail: alloc::format!("domain {name} CHECK ({src:?}) failed to re-parse: {e:?}"),
})?;
let synth_cols = alloc::vec![spg_storage::ColumnSchema::new(
"value",
dom.base_type,
dom.nullable,
)];
let synth_ctx = EvalContext::new(&synth_cols, None);
let synth_row = spg_storage::Row {
values: alloc::vec![v.clone().into_owned()],
};
let r = eval_expr(&expr, &synth_row, &synth_ctx)?;
if matches!(r, Value::Bool(false)) {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"value for domain {name} violates check constraint \"{owner}\""
),
});
}
}
Ok(())
}
#[inline(never)]
pub(crate) fn mysql_operand_reading_pair(
op: BinOp,
l: Value<'static>,
r: Value<'static>,
) -> (Value<'static>, Value<'static>) {
if !mysql_coerces(op) {
return (l, r);
}
match (&l, &r) {
(Value::Text(t), Value::Interval { .. }) => (text_as_temporal(t).unwrap_or(l.clone()), r),
(Value::Interval { .. }, Value::Text(t)) => {
let rr = text_as_temporal(t).unwrap_or(r.clone());
(l, rr)
}
(Value::Bool(b), other)
if mysql_arith(op) && other.data_type().is_some_and(is_numeric_type) =>
{
(Value::BigInt(i64::from(*b)), r)
}
(other, Value::Bool(b))
if mysql_arith(op) && other.data_type().is_some_and(is_numeric_type) =>
{
let rr = Value::BigInt(i64::from(*b));
(l, rr)
}
(Value::Text(t), other) if other.data_type().is_some_and(is_numeric_type) => {
(mysql_number_of(t), r)
}
(other, Value::Text(t)) if other.data_type().is_some_and(is_numeric_type) => {
let rr = mysql_number_of(t);
(l, rr)
}
(Value::Bytes(b), other) if other.data_type().is_some_and(is_numeric_type) => {
(mysql_bytes_as_number(b), r)
}
(other, Value::Bytes(b)) if other.data_type().is_some_and(is_numeric_type) => {
let rr = mysql_bytes_as_number(b);
(l, rr)
}
(Value::Text(a), Value::Text(b)) if mysql_arith(op) => {
(mysql_number_of(a), mysql_number_of(b))
}
_ => (l, r),
}
}
fn mysql_mixed_pair(l: &Value<'_>, r: &Value<'_>) -> bool {
matches!((l, r), (Value::Text(_), o) | (o, Value::Text(_))
if o.data_type().is_some_and(is_numeric_type))
}
const fn mysql_arith(op: BinOp) -> bool {
matches!(
op,
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod
)
}
#[inline(never)]
fn needs_owned_compare(
lc: &Value<'_>,
rc: &Value<'_>,
lhs: &Expr,
rhs: &Expr,
ctx: &EvalContext<'_>,
) -> bool {
is_owned_compare_value(lc)
|| is_owned_compare_value(rc)
|| compare_is_case_insensitive(lhs, rhs, ctx)
|| (ctx.mysql_dialect && mysql_mixed_pair(lc, rc))
}
const fn mysql_coerces(op: BinOp) -> bool {
matches!(
op,
BinOp::Add
| BinOp::Sub
| BinOp::Mul
| BinOp::Div
| BinOp::Mod
| BinOp::Eq
| BinOp::NotEq
| BinOp::Lt
| BinOp::LtEq
| BinOp::Gt
| BinOp::GtEq
)
}
fn is_numeric_type(t: spg_storage::DataType) -> bool {
use spg_storage::DataType as D;
matches!(
t,
D::SmallInt | D::Int | D::BigInt | D::Float | D::Real | D::Numeric { .. }
)
}
fn mysql_collation_key(v: Value<'static>, mysql: bool) -> Value<'static> {
match v {
Value::Text(s) if mysql => Value::text(spg_storage::mysql_compare_fold(&s)),
other => other,
}
}
#[inline(never)]
fn mysql_number_of(s: &str) -> Value<'static> {
let n = mysql_leading_number(s);
if n.fract() == 0.0 && n.abs() < 9.007_199_254_740_992e15 {
#[allow(clippy::cast_possible_truncation)]
Value::BigInt(n as i64)
} else {
Value::Float(n)
}
}
fn mysql_bytes_as_number(b: &[u8]) -> Value<'static> {
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 {
crate::conversions::big_literal_to_value(&alloc::format!("{acc}"))
}
}
#[inline(never)]
pub(crate) fn mysql_true_division(
op: BinOp,
l: &Value<'_>,
r: &Value<'_>,
text_operand: bool,
) -> Option<Value<'static>> {
if matches!(op, BinOp::Mod) {
return if value_is_zero(r) {
Some(Value::Null)
} else {
None
};
}
if !matches!(op, BinOp::Div) {
return None;
}
if text_operand
|| matches!(l, Value::Float(_) | Value::Real(_))
|| matches!(r, Value::Float(_) | Value::Real(_))
{
let f = |v: &Value<'_>| -> Option<f64> {
match v {
Value::Float(x) => Some(*x),
Value::Real(x) => Some(f64::from(*x)),
Value::SmallInt(n) => Some(f64::from(*n)),
Value::Int(n) => Some(f64::from(*n)),
#[allow(clippy::cast_precision_loss)]
Value::BigInt(n) => Some(*n as f64),
_ => None,
}
};
let (a, b) = (f(l)?, f(r)?);
return Some(if b == 0.0 {
Value::Null
} else {
Value::Float(a / b)
});
}
let (ls, lsc) = exact_decimal_parts(l)?;
let (rs, rsc) = exact_decimal_parts(r)?;
if rs == 0 {
return Some(Value::Null);
}
let result_scale = u32::from(lsc) + 4;
let pow = 10i128.checked_pow(u32::from(rsc) + 4)?;
let num = ls.checked_mul(pow)?;
let q = num / rs;
let rem = num % rs;
let bump = if rem.unsigned_abs() * 2 >= rs.unsigned_abs() {
if (num < 0) == (rs < 0) { 1 } else { -1 }
} else {
0
};
Some(Value::numeric(q + bump, u16::try_from(result_scale).ok()?))
}
fn exact_decimal_parts(v: &Value<'_>) -> Option<(i128, u16)> {
match v {
Value::SmallInt(n) => Some((i128::from(*n), 0)),
Value::Int(n) => Some((i128::from(*n), 0)),
Value::BigInt(n) => Some((i128::from(*n), 0)),
Value::Numeric {
scaled,
scale,
kind: spg_storage::NumericKind::Finite,
} => Some((*scaled, *scale)),
_ => None,
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn mysql_bit_u64(v: &Value<'_>) -> Option<u64> {
match v {
Value::SmallInt(n) => Some(i64::from(*n) as u64),
Value::Int(n) => Some(i64::from(*n) as u64),
Value::BigInt(n) => Some(*n as u64),
Value::Bool(b) => Some(u64::from(*b)),
Value::Float(x) => Some(x.round() as i64 as u64),
Value::Real(x) => Some(f64::from(*x).round() as i64 as u64),
Value::Numeric {
scaled,
scale,
kind: spg_storage::NumericKind::Finite,
} => {
if *scale > 38 {
return None;
}
let div = 10i128.pow(u32::from(*scale));
let q = scaled / div;
let rem = scaled % div;
let rounded = if rem.unsigned_abs() * 2 >= div.unsigned_abs() {
q + scaled.signum()
} else {
q
};
Some(rounded as u64)
}
Value::Bytes(b) => mysql_bit_u64(&mysql_bytes_as_number(b)),
Value::Text(s) => mysql_bit_u64(&mysql_number_of(s)),
_ => None,
}
}
fn u64_as_value(n: u64) -> Value<'static> {
match i64::try_from(n) {
Ok(v) => Value::BigInt(v),
Err(_) => Value::numeric(i128::from(n), 0),
}
}
pub(crate) fn mysql_bitwise(op: BinOp, l: &Value<'_>, r: &Value<'_>) -> Option<Value<'static>> {
let out = match op {
BinOp::BitAnd => mysql_bit_u64(l)? & mysql_bit_u64(r)?,
BinOp::BitOr => mysql_bit_u64(l)? | mysql_bit_u64(r)?,
BinOp::BitXor => mysql_bit_u64(l)? ^ mysql_bit_u64(r)?,
BinOp::InetContainedBy => {
let (a, n) = (mysql_bit_u64(l)?, mysql_bit_u64(r)?);
if n >= 64 { 0 } else { a << n }
}
BinOp::InetContains => {
let (a, n) = (mysql_bit_u64(l)?, mysql_bit_u64(r)?);
if n >= 64 { 0 } else { a >> n }
}
_ => return None,
};
Some(u64_as_value(out))
}
pub(crate) fn mysql_bit_not(v: &Value<'_>) -> Option<Value<'static>> {
Some(u64_as_value(!mysql_bit_u64(v)?))
}
pub(crate) fn expr_set_variants<'e>(
e: &'e Expr,
columns: &'e [ColumnSchema],
) -> Option<&'e [String]> {
match e {
Expr::Column(c) => columns
.iter()
.find(|col| col.name == c.name)
.and_then(|col| col.inline_set_variants.as_deref()),
_ => None,
}
}
pub(crate) fn expr_inline_enum_variants<'e>(
e: &'e Expr,
columns: &'e [ColumnSchema],
) -> Option<&'e [String]> {
match e {
Expr::Column(c) => columns
.iter()
.find(|col| col.name == c.name)
.and_then(|col| col.inline_enum_variants.as_deref()),
_ => None,
}
}
pub(crate) fn enum_text_to_ordinal(text: &str, variants: &[String]) -> i64 {
variants
.iter()
.position(|v| v == text)
.map_or(0, |p| p as i64 + 1)
}
pub(crate) fn set_text_to_bitmask(text: &str, variants: &[String]) -> i64 {
if text.is_empty() {
return 0;
}
let mut bits = 0i64;
for member in text.split(',') {
if let Some(pos) = variants.iter().position(|v| v == member) {
bits |= 1i64 << pos;
}
}
bits
}
pub(crate) const fn is_mysql_numeric_binop(op: BinOp) -> bool {
matches!(
op,
BinOp::Add
| BinOp::Sub
| BinOp::Mul
| BinOp::Div
| BinOp::Mod
| BinOp::BitAnd
| BinOp::BitOr
| BinOp::BitXor
| BinOp::InetContainedBy
| BinOp::InetContains
)
}
pub(crate) fn value_is_zero(v: &Value<'_>) -> bool {
match v {
Value::SmallInt(n) => *n == 0,
Value::Int(n) => *n == 0,
Value::BigInt(n) => *n == 0,
Value::Float(x) => *x == 0.0,
Value::Real(x) => *x == 0.0,
Value::Numeric { scaled, .. } => *scaled == 0,
_ => false,
}
}
#[inline(never)]
fn mysql_negate_text(
op: spg_sql::ast::UnOp,
v: &Value<'static>,
) -> Option<Result<Value<'static>, EvalError>> {
match v {
Value::Text(t) => Some(apply_unary(op, mysql_number_of(t))),
_ => None,
}
}
#[inline(never)]
fn mysql_unary_arm(
op: spg_sql::ast::UnOp,
v: &Value<'static>,
) -> Option<Result<Value<'static>, EvalError>> {
use spg_sql::ast::UnOp;
match op {
UnOp::Not if !matches!(v, Value::Bool(_) | Value::Null) => Some(mysql_not(v)),
UnOp::Neg => mysql_negate_text(op, v),
UnOp::Plus => Some(Ok(v.clone())),
UnOp::BitNot if !matches!(v, Value::Null) => mysql_bit_not(v).map(Ok),
_ => None,
}
}
#[inline(never)]
fn text_as_temporal(t: &str) -> Option<Value<'static>> {
parse_timestamp_literal(t)
.map(Value::Timestamp)
.or_else(|| parse_date_literal(t).map(Value::Date))
}
fn is_unknown_string_literal(e: &Expr) -> bool {
matches!(e, Expr::Literal(spg_sql::ast::Literal::String(_)))
}
#[inline(never)]
fn coerce_unknown_literal_to_bool(e: &Expr) -> Result<Value<'static>, EvalError> {
let Expr::Literal(spg_sql::ast::Literal::String(s)) = e else {
unreachable!("guarded by is_unknown_string_literal")
};
cast::cast_value_in(
Value::Text(s.clone().into()),
spg_sql::ast::CastTarget::Bool,
false,
)
}
fn non_boolean_literal_type(e: &Expr) -> Option<&'static str> {
use spg_sql::ast::Literal as L;
match e {
Expr::Literal(L::Integer(_)) => Some("integer"),
Expr::Literal(L::Float(_)) => Some("double precision"),
Expr::Literal(L::Numeric { .. } | L::NumericBig(_)) => Some("numeric"),
_ => None,
}
}
#[inline(never)]
fn eval_connective(
lhs: &Expr,
op: BinOp,
rhs: &Expr,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let side = |e: &Expr| -> Result<Value<'static>, EvalError> {
if is_unknown_string_literal(e) {
coerce_unknown_literal_to_bool(e)
} else {
eval_expr(e, row, ctx)
}
};
let l = side(lhs)?;
if let Some(ty) = non_boolean_literal_type(rhs) {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"argument of {} must be type boolean, not type {ty}",
if matches!(op, BinOp::And) {
"AND"
} else {
"OR"
},
),
});
}
let rhs_resolved = if is_unknown_string_literal(rhs) {
Some(coerce_unknown_literal_to_bool(rhs)?)
} else {
None
};
match (op, &l) {
(BinOp::And, Value::Bool(false)) => return Ok(Value::Bool(false)),
(BinOp::Or, Value::Bool(true)) => return Ok(Value::Bool(true)),
_ => {}
}
let r = match rhs_resolved {
Some(v) => v,
None => side(rhs)?,
};
if matches!(op, BinOp::And) {
and_3vl(l, r)
} else {
apply_binary(op, l, r)
}
}
#[inline(never)]
fn eval_mysql_connective(
lhs: &Expr,
op: BinOp,
rhs: &Expr,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let l = as_mysql_truth(eval_expr(lhs, row, ctx)?)?;
let r = as_mysql_truth(eval_expr(rhs, row, ctx)?)?;
apply_mysql_connective(op, l, r)
}
pub(crate) fn apply_mysql_connective(
op: BinOp,
l: Value<'static>,
r: Value<'static>,
) -> Result<Value<'static>, EvalError> {
if op == BinOp::LogicalXor {
return Ok(match (&l, &r) {
(Value::Bool(a), Value::Bool(b)) => Value::Bool(a != b),
_ => Value::Null,
});
}
apply_binary(op, l, r)
}
#[inline(never)]
pub(crate) fn as_mysql_truth(v: Value<'static>) -> Result<Value<'static>, EvalError> {
Ok(match v {
Value::Null => Value::Null,
other => Value::Bool(predicate_is_true(&other, "AND", true)?),
})
}
#[inline(never)]
fn mysql_not(v: &Value<'_>) -> Result<Value<'static>, EvalError> {
Ok(Value::Bool(!predicate_is_true(v, "NOT", true)?))
}
pub(crate) fn predicate_is_true(v: &Value<'_>, kw: &str, mysql: bool) -> Result<bool, EvalError> {
match v {
Value::Bool(b) => Ok(*b),
Value::Null => Ok(false),
_ if mysql => Ok(mysql_truthy(v)),
Value::Text(t) => match crate::eval::cast::cast_value(
Value::text(t.to_string()),
spg_sql::ast::CastTarget::Bool,
)? {
Value::Bool(b) => Ok(b),
_ => Ok(false),
},
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"argument of {kw} must be type boolean, not type {}",
crate::eval::strings::pg_typeof_name(other)
),
}),
}
}
fn mysql_truthy(v: &Value<'_>) -> bool {
match v {
Value::Bool(b) => *b,
Value::Null => false,
Value::SmallInt(n) => *n != 0,
Value::Int(n) => *n != 0,
Value::BigInt(n) => *n != 0,
Value::Float(f) => *f != 0.0,
Value::Real(f) => *f != 0.0,
Value::Numeric { scaled, .. } => *scaled != 0,
Value::Text(t) => mysql_leading_number(t) != 0.0,
Value::BpChar(t) => mysql_leading_number(t) != 0.0,
_ => true,
}
}
#[inline(never)]
pub(crate) fn mysql_leading_number(s: &str) -> f64 {
let t = s.trim_start();
let mut end = 0usize;
let mut seen_dot = false;
let mut seen_digit = false;
let mut seen_exp = false;
let mut exp_at = 0usize;
for (i, c) in t.char_indices() {
match c {
'-' | '+' if i == 0 => {}
'-' | '+' if seen_exp && i == exp_at + 1 => {}
'0'..='9' => seen_digit = true,
'.' if !seen_dot && !seen_exp => seen_dot = true,
'e' | 'E' if seen_digit && !seen_exp => {
seen_exp = true;
exp_at = i;
}
_ => break,
}
end = i + c.len_utf8();
}
if !seen_digit {
return 0.0;
}
let mut text = &t[..end];
while !text.is_empty() && text.parse::<f64>().is_err() {
text = &text[..text.len() - 1];
}
text.parse::<f64>().unwrap_or(0.0)
}
pub(crate) fn regclass_name_to_oid(cat: &spg_storage::Catalog, bare: &str) -> Option<i64> {
if let Some(oid) = crate::system_catalog::relation_oid(cat, bare) {
return Some(oid);
}
Some(match bare {
"pg_type" => 1247,
"pg_attribute" => 1249,
"pg_proc" => 1255,
"pg_class" => 1259,
"pg_database" => 1262,
"pg_constraint" => 2606,
"pg_index" => 2610,
"pg_namespace" => 2615,
"pg_ts_config" => 3602,
"pg_ts_config_map" => 3603,
"pg_ts_dict" => 3600,
"pg_ts_parser" => 3601,
"pg_ts_template" => 3764,
_ => return None,
})
}
pub(crate) fn apply_composite_cast_pub(
v: Value<'static>,
comp: &spg_storage::CompositeDef,
cat: Option<&spg_storage::Catalog>,
) -> Result<Value<'static>, EvalError> {
apply_composite_cast_in(v, comp, cat)
}
fn coerce_composite_field(
val: Value<'static>,
fname: &str,
fty: spg_storage::DataType,
user_ty: Option<&str>,
cat: Option<&spg_storage::Catalog>,
) -> Result<Value<'static>, EvalError> {
if matches!(val, Value::Null) {
return Ok(val);
}
if let Some(tn) = user_ty
&& let Some(inner) = cat.and_then(|c| c.composite_types().get(tn))
{
return apply_composite_cast_in(val, inner, cat);
}
crate::conversions::coerce_value(val, fty, fname, 0).map_err(|e| EvalError::TypeMismatch {
detail: alloc::format!("{e}"),
})
}
fn apply_composite_cast(
v: Value<'static>,
comp: &spg_storage::CompositeDef,
) -> Result<Value<'static>, EvalError> {
apply_composite_cast_in(v, comp, None)
}
fn apply_composite_cast_in(
v: Value<'static>,
comp: &spg_storage::CompositeDef,
cat: Option<&spg_storage::Catalog>,
) -> Result<Value<'static>, EvalError> {
match v {
Value::Null => Ok(Value::Null),
Value::Composite(fields) => {
if fields.len() != comp.fields.len() {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("cannot cast type record to {}", comp.name),
});
}
let mut out: alloc::vec::Vec<(alloc::string::String, Value<'static>)> =
alloc::vec::Vec::with_capacity(comp.fields.len());
for (i, ((name, fty), (_, val))) in comp.fields.iter().zip(fields).enumerate() {
let ut = comp.field_user_types.get(i).and_then(Option::as_deref);
let coerced = coerce_composite_field(val, name, *fty, ut, cat)?;
out.push((name.clone(), coerced));
}
Ok(Value::Composite(out))
}
Value::Text(s) => {
let raw = parse_record_text(s.as_ref()).ok_or_else(|| EvalError::TypeMismatch {
detail: alloc::format!("malformed record literal: \"{s}\""),
})?;
if raw.len() != comp.fields.len() {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("malformed record literal: \"{s}\""),
});
}
let mut out: alloc::vec::Vec<(alloc::string::String, Value<'static>)> =
alloc::vec::Vec::with_capacity(raw.len());
for (i, ((fname, fty), field_text)) in comp.fields.iter().zip(raw).enumerate() {
let ut = comp.field_user_types.get(i).and_then(Option::as_deref);
let val = match field_text {
None => Value::Null,
Some(t) => coerce_composite_field(Value::text(t), fname, *fty, ut, cat)?,
};
out.push((fname.clone(), val));
}
Ok(Value::Composite(out))
}
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"cannot cast {} to composite type \"{}\"",
crate::conversions::pg_type_name_for_error_opt(other.data_type()),
comp.name
),
}),
}
}
fn parse_record_text(s: &str) -> Option<alloc::vec::Vec<Option<alloc::string::String>>> {
let t = s.trim();
let inner = t.strip_prefix('(')?.strip_suffix(')')?;
let mut out: alloc::vec::Vec<Option<alloc::string::String>> = alloc::vec::Vec::new();
let chars: alloc::vec::Vec<char> = inner.chars().collect();
let mut field = alloc::string::String::new();
let mut quoted_seen = false;
let mut i = 0usize;
let mut in_quotes = false;
loop {
if i >= chars.len() {
if in_quotes {
return None;
}
out.push(if field.is_empty() && !quoted_seen {
None
} else {
Some(field.clone())
});
break;
}
let c = chars[i];
if in_quotes {
match c {
'"' if chars.get(i + 1) == Some(&'"') => {
field.push('"');
i += 2;
}
'"' => {
in_quotes = false;
i += 1;
}
'\\' => {
field.push(*chars.get(i + 1)?);
i += 2;
}
_ => {
field.push(c);
i += 1;
}
}
} else {
match c {
'"' => {
in_quotes = true;
quoted_seen = true;
i += 1;
}
',' => {
out.push(if field.is_empty() && !quoted_seen {
None
} else {
Some(core::mem::take(&mut field))
});
quoted_seen = false;
i += 1;
}
'\\' => {
field.push(*chars.get(i + 1)?);
i += 2;
}
_ => {
field.push(c);
i += 1;
}
}
}
}
Some(out)
}
fn apply_enum_cast<'a>(
v: Value<'a>,
en: &spg_storage::EnumDef,
name: &str,
) -> Result<Value<'a>, EvalError> {
match &v {
Value::Null => Ok(v),
Value::Text(s) => {
if en.labels.iter().any(|l| l.as_str() == s.as_ref()) {
Ok(v)
} else {
Err(EvalError::TypeMismatch {
detail: alloc::format!("invalid input value for enum {name}: {s:?}"),
})
}
}
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"cannot cast {} to enum {name}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
pub(crate) fn expr_enum_type_name_pub<'e>(
e: &'e Expr,
columns: &'e [ColumnSchema],
) -> Option<&'e str> {
expr_enum_type_name(e, columns)
}
pub(crate) fn expr_mysql_fsp(e: &Expr, columns: &[ColumnSchema]) -> Option<u8> {
fn walk(e: &Expr, columns: &[ColumnSchema], best: &mut Option<u8>) {
match e {
Expr::Column(c) => {
if let Some(f) = columns
.iter()
.find(|col| col.name == c.name)
.and_then(|col| col.mysql_fsp)
{
*best = Some(best.map_or(f, |b: u8| b.max(f)));
}
}
Expr::Binary { lhs, rhs, .. } => {
walk(lhs, columns, best);
walk(rhs, columns, best);
}
Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, columns, best),
Expr::FunctionCall { args, .. } => {
for a in args {
walk(a, columns, best);
}
}
Expr::Case {
operand,
branches,
else_branch,
} => {
if let Some(o) = operand.as_deref() {
walk(o, columns, best);
}
for (w, t) in branches {
walk(w, columns, best);
walk(t, columns, best);
}
if let Some(el) = else_branch.as_deref() {
walk(el, columns, best);
}
}
_ => {}
}
}
let mut best = None;
walk(e, columns, &mut best);
best
}
pub(crate) fn expr_is_mysql_unsigned(e: &Expr, columns: &[ColumnSchema]) -> bool {
match e {
Expr::Column(c) => columns
.iter()
.find(|col| col.name == c.name)
.is_some_and(|col| col.is_unsigned),
Expr::Cast {
target: CastTarget::Named(n),
..
} => n.eq_ignore_ascii_case("unsigned"),
Expr::Binary {
lhs,
op: BinOp::Add | BinOp::Sub | BinOp::Mul,
rhs,
} => expr_is_mysql_unsigned(lhs, columns) || expr_is_mysql_unsigned(rhs, columns),
_ => false,
}
}
#[inline(never)]
fn apply_binary_mysql_unsigned(
op: BinOp,
lhs: &Expr,
rhs: &Expr,
l: Value<'static>,
r: Value<'static>,
ctx: &EvalContext,
) -> Result<Value<'static>, EvalError> {
if ctx.mysql_dialect
&& matches!(op, BinOp::Add | BinOp::Sub | BinOp::Mul)
&& let Some(a) = mysql_int_operand(&l)
&& let Some(b) = mysql_int_operand(&r)
&& (expr_is_mysql_unsigned(lhs, ctx.columns) || expr_is_mysql_unsigned(rhs, ctx.columns))
{
let out = match op {
BinOp::Add => a.checked_add(b),
BinOp::Sub => a.checked_sub(b),
_ => a.checked_mul(b),
};
let in_range = out.is_some_and(|v| (0..=i128::from(u64::MAX)).contains(&v));
if !in_range {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"BIGINT UNSIGNED value is out of range in '{}'",
spg_sql::ast::pretty_expr_mysql(&Expr::Binary {
lhs: alloc::boxed::Box::new(lhs.clone()),
op,
rhs: alloc::boxed::Box::new(rhs.clone()),
})
),
});
}
}
let out = apply_binary_in(op, l, r, ctx.mysql_dialect);
if ctx.mysql_dialect && matches!(out, Err(EvalError::DivisionByZero)) {
return Ok(Value::Null);
}
out
}
fn mysql_int_operand(v: &Value<'_>) -> Option<i128> {
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, .. } if *scale == 0 => Some(*scaled),
_ => None,
}
}
fn expr_enum_type_name<'e>(e: &'e Expr, columns: &'e [ColumnSchema]) -> Option<&'e str> {
match e {
Expr::Cast {
target: CastTarget::Named(n),
..
} => Some(n.as_str()),
Expr::Column(c) => columns
.iter()
.find(|col| col.name == c.name)
.and_then(|col| {
col.user_enum_type
.as_deref()
.or(col.user_domain_type.as_deref())
}),
_ => None,
}
}
pub(crate) fn expr_enum_labels<'c>(
e: &Expr,
columns: &[ColumnSchema],
catalog: Option<&'c spg_storage::Catalog>,
) -> Option<&'c [String]> {
let name = expr_enum_type_name(e, columns)?;
catalog
.and_then(|cat| cat.enum_types().get(name))
.map(|en| en.labels.as_slice())
}
pub(crate) fn enum_ord_cmp(
labels: &[String],
a: &Value<'_>,
b: &Value<'_>,
) -> Option<core::cmp::Ordering> {
let pos = |v: &Value<'_>| -> Option<usize> {
match v {
Value::Text(s) => labels.iter().position(|l| l.as_str() == s.as_ref()),
_ => None,
}
};
Some(pos(a)?.cmp(&pos(b)?))
}
#[inline(never)]
fn enum_compare_hook(
op: BinOp,
lhs: &Expr,
rhs: &Expr,
l: &Value<'_>,
r: &Value<'_>,
ctx: &EvalContext<'_>,
) -> Option<Result<Value<'static>, EvalError>> {
let cat = ctx.catalog?;
if cat.enum_types().is_empty() {
return None;
}
let labels = expr_enum_labels(lhs, ctx.columns, ctx.catalog)
.or_else(|| expr_enum_labels(rhs, ctx.columns, ctx.catalog))?;
let ord = enum_ord_cmp(labels, l, r)?;
let b = match op {
BinOp::Eq => ord == core::cmp::Ordering::Equal,
BinOp::NotEq => ord != core::cmp::Ordering::Equal,
BinOp::Lt => ord == core::cmp::Ordering::Less,
BinOp::LtEq => ord != core::cmp::Ordering::Greater,
BinOp::Gt => ord == core::cmp::Ordering::Greater,
BinOp::GtEq => ord != core::cmp::Ordering::Less,
_ => return None,
};
Some(Ok(Value::Bool(b)))
}
#[inline(never)]
fn collate_compare_hook(
op: BinOp,
lhs: &Expr,
rhs: &Expr,
l: &Value<'_>,
r: &Value<'_>,
ctx: &EvalContext<'_>,
) -> Option<Result<Value<'static>, EvalError>> {
if !matches!(op, BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq) {
return None;
}
let (Value::Text(a), Value::Text(b)) = (l, r) else {
return None;
};
let resolve = |c: &spg_sql::ast::ColumnName| -> Option<alloc::string::String> {
let pos = find_column_pos(c, ctx)?;
ctx.columns.get(pos)?.collation_name.clone()
};
let derived = crate::collate_derive::derive(lhs, &resolve)
.combine_pub(crate::collate_derive::derive(rhs, &resolve));
if let Some((x, y)) = derived.conflict() {
return Some(Err(EvalError::TypeMismatch {
detail: alloc::format!(
"collation mismatch between implicit collations \"{x}\" and \"{y}\""
),
}));
}
let ord = crate::collate::compare(derived.name()?, a, b)?;
let b = match op {
BinOp::Lt => ord == core::cmp::Ordering::Less,
BinOp::LtEq => ord != core::cmp::Ordering::Greater,
BinOp::Gt => ord == core::cmp::Ordering::Greater,
BinOp::GtEq => ord != core::cmp::Ordering::Less,
_ => return None,
};
Some(Ok(Value::Bool(b)))
}
#[inline(never)]
fn greatest_least_collation(args: &[Expr], ctx: &EvalContext<'_>) -> Option<alloc::string::String> {
let resolve = |c: &spg_sql::ast::ColumnName| -> Option<alloc::string::String> {
let pos = find_column_pos(c, ctx)?;
ctx.columns.get(pos)?.collation_name.clone()
};
let derived = args
.iter()
.fold(crate::collate_derive::Derived::None, |acc, a| {
acc.combine_pub(crate::collate_derive::derive(a, &resolve))
});
derived
.name()
.filter(|n| crate::collate::is_supported(n))
.map(alloc::string::ToString::to_string)
}
#[cold]
#[inline(never)]
fn unknown_literal_cmp_error(
err: EvalError,
lhs: &Expr,
rhs: &Expr,
lv: &Value<'_>,
rv: &Value<'_>,
) -> EvalError {
let EvalError::TypeMismatch { detail } = &err else {
return err;
};
if !detail.starts_with("operator does not exist")
&& !detail.starts_with("cannot convert text to")
{
return err;
}
let numeric = |v: &Value<'_>| {
matches!(
v.data_type(),
Some(
spg_storage::DataType::SmallInt
| spg_storage::DataType::Int
| spg_storage::DataType::BigInt
| spg_storage::DataType::Float
| spg_storage::DataType::Real
| spg_storage::DataType::Numeric { .. }
)
)
};
let rewrite = |s: &Value<'_>, other: &Value<'_>| -> Option<EvalError> {
let Value::Text(text) = s else { return None };
let dt = other.data_type()?;
Some(EvalError::TypeMismatch {
detail: alloc::format!(
"invalid input syntax for type {}: \"{text}\"",
crate::conversions::pg_type_name_for_error(dt)
),
})
};
if is_unknown_string_literal(lhs)
&& numeric(rv)
&& let Some(e) = rewrite(lv, rv)
{
return e;
}
if is_unknown_string_literal(rhs)
&& numeric(lv)
&& let Some(e) = rewrite(rv, lv)
{
return e;
}
err
}
fn enum_arg_type_name<'e>(args: &'e [Expr], ctx: &EvalContext<'e>) -> Option<&'e str> {
args.iter()
.find_map(|a| expr_enum_type_name(a, ctx.columns))
.filter(|n| {
ctx.catalog
.is_some_and(|cat| cat.enum_types().contains_key(*n))
})
}
#[inline(never)]
fn enum_introspection_applies(args: &[Expr], ctx: &EvalContext<'_>) -> bool {
enum_arg_type_name(args, ctx).is_some()
}
#[inline(never)]
fn eval_enum_introspection(
name: &str,
args: &[Expr],
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let Some(en) = enum_arg_type_name(args, ctx)
.and_then(|n| ctx.catalog.and_then(|cat| cat.enum_types().get(n)))
else {
return Err(EvalError::TypeMismatch {
detail: "could not determine polymorphic type".into(),
});
};
let labels = &en.labels;
if labels.is_empty() {
return Ok(Value::Null);
}
if name.eq_ignore_ascii_case("enum_first") {
return Ok(Value::text(labels[0].clone()));
}
if name.eq_ignore_ascii_case("enum_last") {
return Ok(Value::text(labels[labels.len() - 1].clone()));
}
let pos_of = |v: &Value<'_>| -> Option<usize> {
match v {
Value::Text(s) => labels.iter().position(|l| l == s.as_ref()),
_ => None,
}
};
let (lo, hi) = if args.len() == 2 {
let a = eval_expr(&args[0], row, ctx)?;
let b = eval_expr(&args[1], row, ctx)?;
(
pos_of(&a).unwrap_or(0),
pos_of(&b).unwrap_or(labels.len() - 1),
)
} else {
(0, labels.len() - 1)
};
let out: alloc::vec::Vec<Option<String>> = labels
.get(lo..=hi)
.unwrap_or(&[])
.iter()
.map(|l| Some(l.clone()))
.collect();
Ok(Value::TextArray(out))
}
fn apply_one_subscript(
target_v: Value<'static>,
index: &Expr,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let idx_v = eval_expr(index, row, ctx)?;
if matches!(target_v, Value::Null) || matches!(idx_v, Value::Null) {
return Ok(Value::Null);
}
if matches!(target_v, Value::Json(_)) {
return crate::json::path_get(&target_v, &idx_v, false);
}
let i: i64 = match idx_v {
Value::Int(n) => i64::from(n),
Value::BigInt(n) => n,
Value::SmallInt(n) => i64::from(n),
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"array subscript must be integer, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
if i < 1 {
return Ok(Value::Null);
}
let pos = (i - 1) as usize;
match array_element_at(&target_v, pos) {
Some(v) => Ok(v),
None if array_len(&target_v).is_some() => Ok(Value::Null),
None => Err(EvalError::TypeMismatch {
detail: format!(
"subscript target must be an array, got {}",
crate::conversions::pg_type_name_for_error_opt(target_v.data_type())
),
}),
}
}
fn eval_matrix_subscript(
base: &Value<'static>,
idx_exprs: &[&Expr],
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
if idx_exprs.len() != 2 {
return Ok(Value::Null);
}
let mut idx = [0i64; 2];
for (k, ix) in idx_exprs.iter().enumerate() {
idx[k] = match eval_expr(ix, row, ctx)? {
Value::Null => return Ok(Value::Null),
Value::Int(n) => i64::from(n),
Value::BigInt(n) => n,
Value::SmallInt(n) => i64::from(n),
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"array subscript must be integer, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
}
let (r, c) = (idx[0], idx[1]);
if r < 1 || c < 1 {
return Ok(Value::Null);
}
let (ri, ci) = ((r - 1) as usize, (c - 1) as usize);
macro_rules! elem {
($rows:expr, $map:expr) => {
Ok($rows
.get(ri)
.and_then(|inner| inner.get(ci))
.map_or(Value::Null, |cell| cell.as_ref().map_or(Value::Null, $map)))
};
}
match base {
Value::IntArray2D(rows) => elem!(rows, |n| Value::Int(*n)),
Value::BigIntArray2D(rows) => elem!(rows, |n| Value::BigInt(*n)),
Value::BoolArray2D(rows) => elem!(rows, |b| Value::Bool(*b)),
Value::TextArray2D(rows) => {
elem!(rows, |s| Value::Text(alloc::borrow::Cow::Owned(s.clone())))
}
_ => Ok(Value::Null),
}
}
#[inline(never)]
fn eval_cast_arm(
expr: &Expr,
target: &CastTarget,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let v = eval_expr(expr, row, ctx)?;
if matches!(target, CastTarget::RegClass)
&& let Some(cat) = ctx.catalog
{
let oid = match &v {
Value::Int(n) => Some(i64::from(*n)),
Value::BigInt(n) => Some(*n),
_ => None,
};
if let Some(oid) = oid
&& let Some(name) = crate::system_catalog::relation_name_for_oid(cat, oid)
{
return Ok(Value::text(name));
}
}
if let CastTarget::Named(name) = target
&& let Some(cat) = ctx.catalog
{
if let Some(dom) = cat.domain_types().get(name.as_str()) {
return apply_domain_constraints(v, dom, name, cat);
}
if let Some(en) = cat.enum_types().get(name.as_str()) {
return apply_enum_cast(v, en, name);
}
if let Some(comp) = cat.composite_types().get(name.as_str()) {
return apply_composite_cast_in(v, comp, ctx.catalog);
}
if cat.get(name.as_str()).is_some() {
return if matches!(v, Value::Null) {
Ok(Value::Null)
} else {
Err(EvalError::TypeMismatch {
detail: alloc::format!(
"cannot cast type {} to {name}",
crate::eval::strings::pg_typeof_name(&v),
),
})
};
}
if (name.eq_ignore_ascii_case("regnamespace") || name.eq_ignore_ascii_case("regrole"))
&& let Some(oid) = match &v {
Value::Int(n) => Some(i64::from(*n)),
Value::BigInt(n) => Some(*n),
_ => None,
}
{
let named = if name.eq_ignore_ascii_case("regnamespace") {
crate::system_catalog::schema_name_for_oid(oid)
} else {
ctx.engine.and_then(|e| e.role_name_for_oid(oid))
};
return Ok(Value::text(
named.unwrap_or_else(|| alloc::format!("{oid}")),
));
}
if name.eq_ignore_ascii_case("regnamespace")
&& let Value::Text(t) = &v
{
let want = t.trim().trim_matches('"');
return if spg_storage::is_builtin_schema(want) || cat.schema_exists(want) {
Ok(Value::text(want.to_string()))
} else {
Err(EvalError::TypeMismatch {
detail: alloc::format!("schema \"{want}\" does not exist"),
})
};
}
if name.eq_ignore_ascii_case("regrole")
&& let Value::Text(t) = &v
{
let want = t.trim().trim_matches('"').to_string();
const PREDEFINED: &[&str] = &[
"pg_read_all_data",
"pg_write_all_data",
"pg_monitor",
"pg_read_all_settings",
"pg_read_all_stats",
"pg_stat_scan_tables",
"pg_signal_backend",
"pg_checkpoint",
"pg_maintain",
"pg_use_reserved_connections",
"pg_create_subscription",
];
let known = PREDEFINED.iter().any(|r| r.eq_ignore_ascii_case(&want))
|| ctx.engine.is_some_and(|e| e.role_exists(&want));
return if known {
Ok(Value::text(want))
} else {
Err(EvalError::TypeMismatch {
detail: alloc::format!("role \"{want}\" does not exist"),
})
};
}
if matches!(v, Value::Null)
&& !crate::eval::cast::builtin_target_resolves(name, ctx.mysql_dialect)
{
return Err(EvalError::TypeMismatch {
detail: cast::unknown_type_error_text(name),
});
}
}
if let CastTarget::Named(name) = target
&& name.eq_ignore_ascii_case("record")
{
return match v {
Value::Composite(_) | Value::Null => Ok(v),
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"cannot cast type {} to record",
crate::eval::strings::pg_typeof_name(&other),
),
}),
};
}
if matches!(target, CastTarget::RegClass) {
let oid_in = match &v {
Value::SmallInt(n) => Some(i64::from(*n)),
Value::Int(n) => Some(i64::from(*n)),
Value::BigInt(n) => Some(*n),
_ => None,
};
if let (Some(oid), Some(cat)) = (oid_in, ctx.catalog) {
if oid >= 16384 {
if let Some(name) = cat.table_names().into_iter().nth((oid - 16384) as usize) {
return Ok(Value::RegClass(oid, name.into()));
}
}
}
if let (Value::Text(s), Some(cat)) = (&v, ctx.catalog) {
let bare = s
.rsplit('.')
.next()
.unwrap_or(s)
.trim_matches('"')
.to_string();
if let Some(oid) = regclass_name_to_oid(cat, &bare) {
return Ok(Value::RegClass(oid, bare.into()));
}
const SYSTEM_RELS: &[&str] = &[
"pg_roles",
"pg_user",
"pg_tables",
"pg_views",
"pg_settings",
"pg_stat_activity",
"pg_stat_database",
"pg_stat_user_tables",
"pg_class",
"pg_attribute",
"pg_type",
"pg_proc",
"pg_namespace",
"pg_constraint",
"pg_index",
"pg_rewrite",
];
if !SYSTEM_RELS.contains(&bare.as_str()) {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("relation \"{bare}\" does not exist"),
});
}
}
}
if let (CastTarget::Named(tname), Some(cat), Value::Text(s)) = (target, ctx.catalog, &v) {
let lower = tname.to_ascii_lowercase();
if matches!(lower.as_str(), "regproc" | "regprocedure") {
let raw = s.trim();
let (name_part, args_part) = match raw.split_once('(') {
Some((n, rest)) => (n.trim(), Some(rest.trim_end_matches(')'))),
None => (raw, None),
};
let bare = name_part
.strip_prefix("public.")
.unwrap_or(name_part)
.trim_matches('"');
let cands = cat.functions_named(bare);
if let Some(args_txt) = args_part {
let want =
crate::system_catalog::canonical_arg_types(&alloc::format!("({args_txt})"));
if let Some(f) = cands
.iter()
.find(|f| crate::system_catalog::canonical_arg_types(&f.args_repr) == want)
{
let rendered = alloc::format!(
"{bare}({})",
crate::system_catalog::canonical_arg_types(&f.args_repr)
);
let oid = crate::system_catalog::function_oid_by_signature(cat, bare, &want)
.unwrap_or(0);
return Ok(Value::RegProc(oid, rendered.into()));
}
} else {
match cands.len() {
0 => {}
1 => {
let oid = crate::system_catalog::function_oid(cat, bare).unwrap_or(0);
return Ok(Value::RegProc(oid, bare.into()));
}
_ => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("more than one function named \"{bare}\""),
});
}
}
}
}
}
if matches!(target, CastTarget::Text)
&& let Value::Timestamp(t) = &v
&& crate::describe::describe_expr(expr, ctx.columns)
.is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
{
let off = ctx.session_tz_offset_at(*t);
let abbr = ctx.session_tz_abbrev_at(*t);
return Ok(Value::text(format::format_timestamptz_tz(
*t,
&ctx.render_style,
off,
abbr.as_deref(),
)));
}
if matches!(
target,
CastTarget::Date | CastTarget::Timestamp | CastTarget::Timestamptz
) && let Value::Text(word) = &v
&& let Some(clock) = ctx.clock
{
let w = word.trim().to_ascii_lowercase();
if matches!(w.as_str(), "today" | "tomorrow" | "yesterday" | "now") {
let now_us = clock();
let today = i32::try_from(now_us.div_euclid(86_400_000_000)).ok();
if let Some(today) = today {
let day = match w.as_str() {
"tomorrow" => today + 1,
"yesterday" => today - 1,
_ => today,
};
return Ok(match (&target, w.as_str()) {
(CastTarget::Date, _) => Value::Date(day),
(_, "now") => Value::Timestamp(now_us),
_ => Value::Timestamp(crate::conversions::date_days_to_micros(day)),
});
}
}
}
if ctx.render_style.date_order != format::DateOrder::Mdy {
match (&target, &v) {
(CastTarget::Date, Value::Text(s)) => {
if let Some(d) = format::parse_date_literal_ordered(s, ctx.render_style.date_order)
{
return Ok(Value::Date(d));
}
}
(CastTarget::Timestamp, Value::Text(s)) => {
if let Some(t) =
format::parse_timestamp_literal_ordered(s, ctx.render_style.date_order)
{
return Ok(Value::Timestamp(t));
}
}
_ => {}
}
}
let zoneless_target = match &target {
CastTarget::Timestamp => Some("timestamp"),
CastTarget::Date => Some("date"),
CastTarget::Named(n) if n.eq_ignore_ascii_case("time") => Some("time"),
_ => None,
};
if let Some(kind) = zoneless_target
&& let Value::Text(txt) = &v
&& let Some((wall, zone)) = split_trailing_zone_name(txt, ctx.render_style.date_order)
{
if ctx.zone_local_to_utc(zone, wall).is_none() {
if zone.contains('/') {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"time zone \"{}\" not recognized",
zone.to_ascii_lowercase()
),
});
}
} else {
return Ok(match kind {
"date" => {
Value::Date(i32::try_from(wall.div_euclid(86_400_000_000)).map_err(|_| {
EvalError::TypeMismatch {
detail: "timestamp out of DATE range".into(),
}
})?)
}
"time" => Value::Time(wall.rem_euclid(86_400_000_000)),
_ => Value::Timestamp(wall),
});
}
}
if matches!(target, CastTarget::Timestamptz)
&& let Value::Text(txt) = &v
{
let order = ctx.render_style.date_order;
let sess_zone = ctx
.session_gucs
.and_then(|g| g.get("timezone"))
.map(String::as_str);
if let Some(idx) = txt.trim_end().rfind(' ') {
let (head, tail) = (txt[..idx].trim(), txt[idx + 1..].trim());
let tail_is_zoneish = tail.len() > 1
&& tail.bytes().any(|b| b.is_ascii_alphabetic())
&& !tail.eq_ignore_ascii_case("bc")
&& !tail.eq_ignore_ascii_case("ad");
if tail_is_zoneish
&& format::parse_timestamp_literal_tz_ordered(txt, order).is_none()
&& let Some((wall, false)) = format::parse_timestamp_literal_tz_ordered(head, order)
&& let Some(utc) = ctx.zone_local_to_utc(tail, wall)
{
return Ok(Value::Timestamp(utc));
}
}
if let Some((wall, had_tz)) = format::parse_timestamp_literal_tz_ordered(txt, order) {
if had_tz {
return Ok(Value::Timestamp(wall));
}
if let Some(zone) = sess_zone
&& !zone.eq_ignore_ascii_case("utc")
&& !zone.eq_ignore_ascii_case("gmt")
&& let Some(utc) = ctx.zone_local_to_utc(zone, wall)
{
return Ok(Value::Timestamp(utc));
}
return Ok(Value::Timestamp(wall));
}
}
if matches!(target, CastTarget::Timestamptz)
&& let Value::Timestamp(wall) = &v
&& !matches!(
crate::describe::describe_expr(expr, ctx.columns).map(|s| s.ty),
Some(spg_storage::DataType::Timestamptz)
)
&& let Some(zone) = ctx.session_gucs.and_then(|g| g.get("timezone"))
&& !zone.eq_ignore_ascii_case("utc")
&& !zone.eq_ignore_ascii_case("gmt")
&& let Some(utc) = ctx.zone_local_to_utc(zone, *wall)
{
return Ok(Value::Timestamp(utc));
}
if matches!(target, CastTarget::Timestamp | CastTarget::Date)
&& let Value::Timestamp(t) = &v
&& crate::describe::describe_expr(expr, ctx.columns)
.is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
{
let local = t.saturating_add(ctx.session_tz_offset_at(*t));
return Ok(match target {
CastTarget::Date => i32::try_from(local.div_euclid(86_400_000_000))
.map_or(Value::Timestamp(local), Value::Date),
_ => Value::Timestamp(local),
});
}
if matches!(target, CastTarget::Text) {
match &v {
Value::Bytes(b) if ctx.render_style.bytea_escape => {
return Ok(Value::text(format::format_bytea_escape(b)));
}
Value::Date(d) => {
return Ok(Value::text(format::format_date_styled(
*d,
&ctx.render_style,
)));
}
Value::Timestamp(t) => {
return Ok(Value::text(format::format_timestamp_styled(
*t,
&ctx.render_style,
)));
}
Value::Interval {
months,
days,
micros,
} => {
return Ok(Value::text(format::format_interval_styled(
*months,
*days,
*micros,
&ctx.render_style,
)));
}
Value::Float(x) => {
return Ok(Value::text(format::format_float_styled(
*x,
&ctx.render_style,
)));
}
Value::Real(x) => {
return Ok(Value::text(format::format_real_styled(
*x,
&ctx.render_style,
)));
}
_ => {}
}
}
crate::eval::cast::cast_value_ref_in(v, target, ctx.mysql_dialect)
}
#[inline(never)]
fn eval_array_arm(
items: &[Expr],
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let mut materialised: Vec<Value<'static>> = Vec::with_capacity(items.len());
for elem in items {
materialised.push(eval_expr(elem, row, ctx)?);
}
let all_arrays = !materialised.is_empty()
&& materialised.iter().all(|v| {
values::array_len(v).is_some()
&& !matches!(
v,
Value::TextArray2D(_)
| Value::IntArray2D(_)
| Value::BigIntArray2D(_)
| Value::BoolArray2D(_)
)
});
if all_arrays {
let row_len = values::array_len(&materialised[0]).unwrap_or(0);
let same_len = materialised
.iter()
.all(|v| values::array_len(v) == Some(row_len));
if !same_len {
return Err(EvalError::TypeMismatch {
detail: "multidimensional arrays must have array expressions \
with matching dimensions"
.into(),
});
}
if materialised
.iter()
.all(|v| matches!(v, Value::BoolArray(_)))
{
let rows: Vec<Vec<Option<bool>>> = materialised
.into_iter()
.map(|v| match v {
Value::BoolArray(r) => r,
_ => unreachable!("checked above"),
})
.collect();
return Ok(Value::BoolArray2D(rows));
}
let any_text = materialised
.iter()
.any(|v| !matches!(v, Value::IntArray(_) | Value::BigIntArray(_)));
let any_big = materialised
.iter()
.any(|v| matches!(v, Value::BigIntArray(_)));
if any_text {
let rows: Vec<Vec<Option<String>>> = materialised
.into_iter()
.map(|v| match v {
Value::TextArray(r) => r,
other => {
let n = values::array_len(&other).unwrap_or(0);
(0..n)
.map(|i| match values::array_element_at(&other, i) {
None | Some(Value::Null) => None,
Some(v) => Some(value_to_text(&v)),
})
.collect()
}
})
.collect();
return Ok(Value::TextArray2D(rows));
}
if any_big {
let rows: Vec<Vec<Option<i64>>> = materialised
.into_iter()
.map(|v| match v {
Value::BigIntArray(r) => r,
Value::IntArray(r) => r.into_iter().map(|c| c.map(i64::from)).collect(),
_ => unreachable!(),
})
.collect();
return Ok(Value::BigIntArray2D(rows));
}
let rows: Vec<Vec<Option<i32>>> = materialised
.into_iter()
.map(|v| match v {
Value::IntArray(r) => r,
_ => unreachable!(),
})
.collect();
return Ok(Value::IntArray2D(rows));
}
if let Some(v) = values::homogeneous_typed_array(&materialised) {
return Ok(crate::describe::upgrade_timestamptz_array(
v,
items,
ctx.columns,
));
}
unify_array_elements(items, &mut materialised)?;
if let Some(v) = values::homogeneous_typed_array(&materialised) {
return Ok(crate::describe::upgrade_timestamptz_array(
v,
items,
ctx.columns,
));
}
let mut has_text = false;
let mut has_float = false;
let mut has_numeric = false;
let mut has_bigint = false;
let mut has_int = false;
let mut numeric_representable = true;
for v in &materialised {
match v {
Value::Null => {}
Value::Int(_) | Value::SmallInt(_) => has_int = true,
Value::BigInt(_) => has_bigint = true,
Value::Numeric {
kind: spg_storage::NumericKind::Finite,
..
} => {
has_numeric = true;
}
Value::Numeric { .. } => {
has_numeric = true;
numeric_representable = false;
}
Value::NumericBig(_) => {
has_numeric = true;
numeric_representable = false;
}
Value::Float(_) => has_float = true,
Value::Text(_) | Value::Json(_) => has_text = true,
Value::RegClass(..) | Value::RegProc(..) | Value::RegType(..) => has_bigint = true,
_ => has_text = true,
}
}
let any_numlike = has_int || has_bigint || has_numeric || has_float;
if has_text || !any_numlike || (has_numeric && !numeric_representable) {
let out: Vec<Option<String>> = materialised
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Text(s) | Value::Json(s) => Some(s.into_owned()),
other => Some(value_to_text_for_array(&other, &ctx.render_style)),
})
.collect();
return Ok(Value::TextArray(out));
}
if has_float {
let out: Vec<Option<f64>> = materialised
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Float(f) => Some(f),
Value::Int(n) => Some(f64::from(n)),
Value::SmallInt(n) => Some(f64::from(n)),
#[allow(clippy::cast_precision_loss)]
Value::BigInt(n) => Some(n as f64),
#[allow(clippy::cast_precision_loss)]
Value::Numeric { scaled, scale, .. } => {
Some(scaled as f64 / libm::pow(10.0, f64::from(scale)))
}
_ => None,
})
.collect();
return Ok(Value::FloatArray(out));
}
if has_numeric {
let out: Vec<Option<(i128, u16)>> = materialised
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::SmallInt(n) => Some((i128::from(n), 0)),
Value::Int(n) => Some((i128::from(n), 0)),
Value::BigInt(n) => Some((i128::from(n), 0)),
Value::Numeric { scaled, scale, .. } => Some((scaled, scale)),
_ => None,
})
.collect();
return Ok(Value::NumericArray(out));
}
if has_bigint {
let out: Vec<Option<i64>> = materialised
.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),
Value::RegClass(oid, _) | Value::RegProc(oid, _) | Value::RegType(oid, _) => {
Some(oid)
}
_ => unreachable!(),
})
.collect();
return Ok(Value::BigIntArray(out));
}
let out: Vec<Option<i32>> = materialised
.into_iter()
.map(|v| match v {
Value::Null => None,
Value::Int(n) => Some(n),
Value::SmallInt(n) => Some(i32::from(n)),
_ => unreachable!(),
})
.collect();
Ok(Value::IntArray(out))
}
#[inline(never)]
fn eval_function_call_arm(
name: &str,
args: &[Expr],
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
if args.iter().any(|a| matches!(a, Expr::Variadic(_))) {
let expanded = expand_variadic_args(args, row, ctx)?;
return eval_function_call_arm(name, &expanded, row, ctx);
}
if name.eq_ignore_ascii_case("date_part")
&& args.len() == 2
&& let Expr::Literal(spg_sql::ast::Literal::String(unit)) = &args[0]
&& matches!(
unit.to_ascii_lowercase().as_str(),
"timezone" | "timezone_hour" | "timezone_minute"
)
&& matches!(&args[1], Expr::Cast { .. } | Expr::Column(_))
&& let Some(sch) = crate::describe::describe_expr(&args[1], ctx.columns)
&& matches!(sch.ty, spg_storage::DataType::Timestamp)
{
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"unit \"{}\" not supported for type timestamp without time zone",
unit.to_ascii_lowercase()
),
});
}
if name.eq_ignore_ascii_case("pg_typeof")
&& let [arg] = args
{
let is_user_type = |e: &Expr| {
expr_enum_type_name(e, ctx.columns)
.filter(|n| {
crate::system_catalog::is_information_schema_domain(n)
|| ctx.catalog.is_some_and(|cat| {
cat.enum_types().contains_key(*n)
|| cat.domain_types().contains_key(*n)
|| cat.composite_types().contains_key(*n)
})
})
.map(alloc::string::String::from)
};
let is_enum = is_user_type;
if let Some(en) = is_enum(arg) {
return Ok(Value::text(en));
}
if let Expr::Array(items) = arg
&& let Some(first) = items.first()
&& let Some(en) = is_enum(first)
{
return Ok(Value::text(alloc::format!("{en}[]")));
}
}
if args.iter().any(|a| matches!(a, Expr::NamedArg { .. })) {
let positional = resolve_named_args(name, args, ctx)?;
return eval_function_call_arm(name, &positional, row, ctx);
}
eval_function_call_positional(name, args, row, ctx)
}
fn expand_variadic_args(
args: &[Expr],
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<alloc::vec::Vec<Expr>, EvalError> {
let mut out = alloc::vec::Vec::with_capacity(args.len());
for a in args {
if let Expr::Variadic(inner) = a {
let v = eval_expr(inner, row, ctx)?;
let elems = crate::select::array_value_to_elements(&v).map_err(|_| {
EvalError::TypeMismatch {
detail: "VARIADIC argument must be an array".into(),
}
})?;
for e in elems {
out.push(Expr::Literal(crate::value_to_literal(e)));
}
} else {
out.push(a.clone());
}
}
Ok(out)
}
fn declared_param_names(fname: &str, ctx: &EvalContext<'_>) -> Option<alloc::vec::Vec<String>> {
let lower = fname.to_ascii_lowercase();
let builtin: &[&str] = match lower.as_str() {
"make_date" => &["year", "month", "day"],
"make_time" => &["hour", "min", "sec"],
"make_timestamp" | "make_timestamptz" => &["year", "month", "mday", "hour", "min", "sec"],
"make_interval" => &["years", "months", "weeks", "days", "hours", "mins", "secs"],
_ => &[],
};
if !builtin.is_empty() {
return Some(builtin.iter().map(|s| (*s).to_string()).collect());
}
let cat = ctx.catalog?;
let def = cat
.functions()
.values()
.find(|f| f.name.eq_ignore_ascii_case(&lower))?;
let names = spg_storage::function_arg_names(&def.args_repr);
if names.iter().all(alloc::string::String::is_empty) {
return None;
}
Some(names)
}
fn resolve_named_args(
fname: &str,
args: &[Expr],
ctx: &EvalContext<'_>,
) -> Result<alloc::vec::Vec<Expr>, EvalError> {
let Some(params) = declared_param_names(fname, ctx) else {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("function {fname}(...) does not support named arguments"),
});
};
let mut slots: alloc::vec::Vec<Option<Expr>> = (0..params.len()).map(|_| None).collect();
let mut next_positional = 0usize;
for a in args {
let (idx, val) = match a {
Expr::NamedArg { name, expr } => {
let i = params
.iter()
.position(|p| p.eq_ignore_ascii_case(name))
.ok_or_else(|| EvalError::TypeMismatch {
detail: alloc::format!("{fname}(...) has no argument named \"{name}\""),
})?;
(i, (**expr).clone())
}
other => {
let i = next_positional;
next_positional += 1;
(i, other.clone())
}
};
if idx >= slots.len() {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("{fname}(...) got too many arguments"),
});
}
if slots[idx].is_some() {
return Err(EvalError::TypeMismatch {
detail: alloc::format!("{fname}(...) got multiple values for one argument"),
});
}
slots[idx] = Some(val);
}
let make_family = fname.to_ascii_lowercase().starts_with("make_");
let mut out = alloc::vec::Vec::with_capacity(slots.len());
for slot in slots {
match slot {
Some(e) => out.push(e),
None if make_family => {
out.push(Expr::Literal(spg_sql::ast::Literal::Integer(0)));
}
None => {}
}
}
Ok(out)
}
fn eval_function_call_positional(
name: &str,
args: &[Expr],
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
if matches!(args.len(), 2..) {
let construct = if name.eq_ignore_ascii_case("coalesce") {
Some("COALESCE")
} else if name.eq_ignore_ascii_case("greatest") {
Some("GREATEST")
} else if name.eq_ignore_ascii_case("least") {
Some("LEAST")
} else {
None
};
if let Some(construct) = construct {
unify_branch_types_static(construct, args.iter(), ctx)?;
}
}
if (name.eq_ignore_ascii_case("enum_first")
|| name.eq_ignore_ascii_case("enum_last")
|| name.eq_ignore_ascii_case("enum_range"))
&& enum_introspection_applies(args, ctx)
{
return eval_enum_introspection(name, args, row, ctx);
}
if args.len() == 2
&& name.eq_ignore_ascii_case("to_char")
&& let Some(zone) = ctx.session_gucs.and_then(|g| g.get("timezone"))
&& !zone.eq_ignore_ascii_case("utc")
&& !zone.eq_ignore_ascii_case("gmt")
&& crate::describe::describe_expr(&args[0], ctx.columns)
.is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
&& let Value::Timestamp(t) = eval_expr(&args[0], row, ctx)?
{
let off = ctx.session_tz_offset_at(t);
let abbrev = ctx
.session_tz_abbrev_at(t)
.unwrap_or_else(|| zone.to_uppercase());
let vals = [
Value::Timestamp(t.saturating_add(off)),
eval_expr(&args[1], row, ctx)?,
];
return crate::eval::strings::to_char_in_zone(&vals, Some((&abbrev, off)));
}
if args.len() == 2
&& (name.eq_ignore_ascii_case("date_trunc") || name.eq_ignore_ascii_case("date_bin"))
&& let Some(zone) = ctx.session_gucs.and_then(|g| g.get("timezone"))
&& !zone.eq_ignore_ascii_case("utc")
&& !zone.eq_ignore_ascii_case("gmt")
&& crate::describe::describe_expr(&args[1], ctx.columns)
.is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
{
let vals = [
eval_expr(&args[0], row, ctx)?,
eval_expr(&args[1], row, ctx)?,
Value::text(zone.clone()),
];
return datetime::date_trunc(&vals, ctx);
}
if args.len() == 2
&& name.eq_ignore_ascii_case("timezone")
&& let zone_v = eval_expr(&args[0], row, ctx)?
&& let Value::Text(zone) = &zone_v
&& datetime::resolve_zone_offset(zone.as_ref()).is_none()
&& !zone.trim().eq_ignore_ascii_case("utc")
&& !zone.trim().eq_ignore_ascii_case("gmt")
&& zone.parse::<i64>().is_err()
&& ctx.tz_offset_fn.is_some()
{
let zone = zone.trim();
let src_is_tstz = crate::describe::describe_expr(&args[1], ctx.columns)
.is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz));
let inner = eval_expr(&args[1], row, ctx)?;
if let Value::Timestamp(t) = inner {
if src_is_tstz {
if let Some(off) = ctx.zone_offset_at(zone, t) {
return Ok(Value::Timestamp(t + off));
}
} else if let Some(utc) = ctx.zone_local_to_utc(zone, t) {
return Ok(Value::Timestamp(utc));
}
return Err(EvalError::TypeMismatch {
detail: alloc::format!("time zone \"{zone}\" not recognized"),
});
}
}
if args.len() == 2
&& name.eq_ignore_ascii_case("left")
&& let Expr::Column(c) = &args[0]
&& let Some(cell) = resolve_column_borrowed(c, row, ctx)?
{
{
match cell {
Value::Null => return Ok(Value::Null),
Value::Text(t) => {
let n_v = eval_expr(&args[1], row, ctx)?;
if let Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) = n_v {
let n = match n_v {
Value::SmallInt(x) => i64::from(x),
Value::Int(x) => i64::from(x),
Value::BigInt(x) => x,
_ => 0,
};
return Ok(Value::text(text_prefix_chars(t, n)));
}
}
_ => {}
}
}
}
if args.len() == 1
&& name.eq_ignore_ascii_case("pg_typeof")
&& crate::describe::describe_expr(&args[0], ctx.columns)
.is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
{
return Ok(Value::text::<alloc::string::String>(
"timestamp with time zone".into(),
));
}
if args.len() == 1
&& name.eq_ignore_ascii_case("pg_typeof")
&& crate::describe::describe_expr(&args[0], ctx.columns)
.is_some_and(|s| matches!(s.ty, spg_storage::DataType::OidArray))
{
return Ok(Value::text::<alloc::string::String>("oid[]".into()));
}
if args.len() == 1
&& name.eq_ignore_ascii_case("pg_typeof")
&& let Expr::Column(c) = &args[0]
&& let Some(cname) = ctx
.columns
.iter()
.find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
.and_then(|sc| sc.user_composite_type.as_deref())
{
return Ok(Value::text::<alloc::string::String>(cname.into()));
}
if args.len() == 1
&& name.eq_ignore_ascii_case("pg_typeof")
&& let Expr::Column(c) = &args[0]
&& ctx
.columns
.iter()
.any(|sc| sc.name.eq_ignore_ascii_case(&c.name) && sc.ty == spg_storage::DataType::Name)
{
return Ok(Value::text::<alloc::string::String>("name".into()));
}
if args.len() == 1
&& name.eq_ignore_ascii_case("pg_typeof")
&& let Expr::Cast {
target: spg_sql::ast::CastTarget::Named(n),
..
} = &args[0]
&& n.eq_ignore_ascii_case("name")
{
return Ok(Value::text::<alloc::string::String>("name".into()));
}
if args.len() == 1
&& name.eq_ignore_ascii_case("pg_typeof")
&& matches!(&args[0], Expr::Literal(spg_sql::ast::Literal::String(_)))
{
return Ok(Value::text::<alloc::string::String>("unknown".into()));
}
if args.len() == 1 && name.eq_ignore_ascii_case("pg_typeof") {
if matches!(&args[0], Expr::Literal(spg_sql::ast::Literal::Null)) {
return Ok(Value::text::<alloc::string::String>("unknown".into()));
}
let v = eval_expr(&args[0], row, ctx)?;
if matches!(v, Value::Null)
&& let Some(shape) = crate::describe::describe_expr(&args[0], ctx.columns)
&& let Some(n) = pg_typeof_name_for_datatype(shape.ty)
{
return Ok(Value::text(n));
}
if !matches!(v, Value::Null)
&& let Some(shape) = crate::describe::describe_expr(&args[0], ctx.columns)
&& matches!(
shape.ty,
spg_storage::DataType::Xid
| spg_storage::DataType::Xid8
| spg_storage::DataType::Oid
)
&& let Some(n) = pg_typeof_name_for_datatype(shape.ty)
{
return Ok(Value::text(n));
}
return apply_function(name, &[v], ctx);
}
if name.eq_ignore_ascii_case("coalesce") && !args.is_empty() {
let mut result: Option<Value<'static>> = None;
let mut tbuf = [spg_storage::DataType::Int; 8];
let mut ntypes = 0usize;
let mut spill: Vec<spg_storage::DataType> = Vec::new();
for a in args {
let v = if result.is_none() {
eval_expr(a, row, ctx)?
} else {
match eval_expr(a, row, ctx) {
Ok(v) => v,
Err(_) => continue,
}
};
let branch_ty = match v.data_type() {
Some(t) => Some(t),
None => crate::describe::describe_expr(a, ctx.columns).map(|sh| sh.ty),
};
if let Some(t) = branch_ty {
if ntypes < tbuf.len() {
tbuf[ntypes] = t;
ntypes += 1;
} else {
spill.push(t);
}
}
if result.is_none() && !matches!(v, Value::Null) {
result = Some(v);
}
}
let result = result.unwrap_or(Value::Null);
if matches!(result, Value::Text(_)) {
if let Some(target) = args.iter().find_map(coalesce_type_hint) {
return crate::eval::cast::cast_value(result, target);
}
}
if spill.is_empty() {
return Ok(widen_to_common(result, &tbuf[..ntypes]));
}
let mut types: Vec<spg_storage::DataType> = tbuf[..ntypes].to_vec();
types.append(&mut spill);
return Ok(widen_to_common(result, &types));
}
let evaluated: Result<Vec<Value<'static>>, _> =
args.iter().map(|a| eval_expr(a, row, ctx)).collect();
let evaluated = evaluated?;
if (name.eq_ignore_ascii_case("to_json") || name.eq_ignore_ascii_case("to_jsonb"))
&& evaluated.len() == 1
&& let Some(Value::Timestamp(t)) = evaluated.first()
&& args.first().is_some_and(|a| {
crate::describe::describe_expr(a, ctx.columns)
.is_some_and(|sh| matches!(sh.ty, spg_storage::DataType::Timestamptz))
})
{
let off = ctx.session_tz_offset_at(*t);
let local = t + off;
let days = local.div_euclid(86_400_000_000);
let day_us = local.rem_euclid(86_400_000_000);
let (y, mo, d) = civil_from_days(i32::try_from(days).unwrap_or(0));
let secs = day_us / 1_000_000;
let frac = day_us % 1_000_000;
let (hh, mi, ss) = (secs / 3600, (secs / 60) % 60, secs % 60);
let mut txt = alloc::format!("{y:04}-{mo:02}-{d:02}T{hh:02}:{mi:02}:{ss:02}");
if frac != 0 {
let f = alloc::format!("{frac:06}");
txt.push('.');
txt.push_str(f.trim_end_matches('0'));
}
let (sign, omag) = if off < 0 { ('-', -off) } else { ('+', off) };
let (oh, om) = (omag / 3_600_000_000, (omag / 60_000_000) % 60);
let _ = core::fmt::Write::write_fmt(&mut txt, format_args!("{sign}{oh:02}:{om:02}"));
return Ok(Value::json(alloc::format!("\"{txt}\"")));
}
if (name.eq_ignore_ascii_case("greatest") || name.eq_ignore_ascii_case("least"))
&& let Some(labels) = args
.iter()
.find_map(|a| expr_enum_labels(a, ctx.columns, ctx.catalog))
&& evaluated
.iter()
.all(|v| matches!(v, Value::Text(_) | Value::Null))
{
let is_greatest = name.eq_ignore_ascii_case("greatest");
let mut best: Option<&Value<'static>> = None;
for v in evaluated.iter().filter(|v| !matches!(v, Value::Null)) {
best = Some(match best {
None => v,
Some(b) => match enum_ord_cmp(labels, v, b) {
Some(core::cmp::Ordering::Greater) if is_greatest => v,
Some(core::cmp::Ordering::Less) if !is_greatest => v,
Some(_) => b,
None => return apply_function(name, &evaluated, ctx),
},
});
}
return Ok(best.cloned().unwrap_or(Value::Null));
}
if (name.eq_ignore_ascii_case("greatest") || name.eq_ignore_ascii_case("least"))
&& evaluated
.iter()
.all(|v| matches!(v, Value::Text(_) | Value::Null))
&& let Some(coll) = greatest_least_collation(args, ctx)
{
let is_greatest = name.eq_ignore_ascii_case("greatest");
let mut best: Option<&Value<'static>> = None;
for v in evaluated.iter().filter(|v| !matches!(v, Value::Null)) {
best = Some(match (best, v) {
(None, _) => v,
(Some(Value::Text(y)), Value::Text(x)) => {
match crate::collate::compare(&coll, x, y) {
Some(core::cmp::Ordering::Greater) if is_greatest => v,
Some(core::cmp::Ordering::Less) if !is_greatest => v,
Some(_) => best.unwrap_or(v),
None => return apply_function(name, &evaluated, ctx),
}
}
(Some(b), _) => b,
});
}
return Ok(best.cloned().unwrap_or(Value::Null));
}
if let Some(want) = unknown_literal_param_type(name) {
let mut coerced = evaluated;
for (i, a) in args.iter().enumerate() {
if is_unknown_string_literal(a)
&& let Some(slot) = coerced.get_mut(i)
{
*slot = cast::cast_value_in(
core::mem::replace(slot, Value::Null),
want.clone(),
false,
)?;
}
}
return apply_function(name, &coerced, ctx);
}
apply_function(name, &evaluated, ctx)
}
fn unknown_literal_param_type(name: &str) -> Option<spg_sql::ast::CastTarget> {
match name.to_ascii_lowercase().as_str() {
"justify_days" | "justify_hours" | "justify_interval" => {
Some(spg_sql::ast::CastTarget::Interval)
}
_ => None,
}
}
#[inline(never)]
fn eval_any_all_arm(
expr: &Expr,
op: &BinOp,
array: &Expr,
is_any: bool,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let lhs = eval_expr(expr, row, ctx)?;
let arr = eval_expr(array, row, ctx)?;
any_all_over(lhs, arr, op, is_any)
}
pub(crate) fn any_all_over(
lhs: Value<'static>,
arr: Value<'static>,
op: &BinOp,
is_any: bool,
) -> Result<Value<'static>, EvalError> {
if matches!(arr, Value::Null) {
return Ok(Value::Null);
}
let arr = match &arr {
Value::Text(_) => {
let arr_ty = match lhs.data_type() {
Some(spg_storage::DataType::SmallInt) => spg_storage::DataType::SmallIntArray,
Some(spg_storage::DataType::Int) => spg_storage::DataType::IntArray,
Some(spg_storage::DataType::BigInt) => spg_storage::DataType::BigIntArray,
Some(spg_storage::DataType::Numeric { .. }) => spg_storage::DataType::NumericArray,
Some(spg_storage::DataType::Float) => spg_storage::DataType::FloatArray,
Some(spg_storage::DataType::Bool) => spg_storage::DataType::BoolArray,
Some(spg_storage::DataType::Date) => spg_storage::DataType::DateArray,
_ => spg_storage::DataType::TextArray,
};
crate::conversions::coerce_value(arr.clone(), arr_ty, "", 0).unwrap_or(arr)
}
_ => arr,
};
let Some(len) = array_len(&arr) else {
return Err(EvalError::TypeMismatch {
detail: format!(
"ANY/ALL right-hand side must be an array, got {}",
crate::conversions::pg_type_name_for_error_opt(arr.data_type())
),
});
};
let elems: Vec<Option<Value>> = (0..len)
.map(|i| match array_element_at(&arr, i) {
Some(Value::Null) | None => None,
Some(v) => Some(v),
})
.collect();
if elems.is_empty() {
return Ok(Value::Bool(!is_any));
}
let mut saw_null = matches!(lhs, Value::Null);
let mut saw_match = false;
let mut saw_mismatch = false;
for elem in elems {
let elem_v = match elem {
Some(v) => v,
None => {
saw_null = true;
continue;
}
};
if matches!(lhs, Value::Null) {
saw_null = true;
continue;
}
match apply_binary(*op, lhs.clone(), elem_v) {
Ok(Value::Bool(true)) => saw_match = true,
Ok(Value::Bool(false)) => saw_mismatch = true,
Ok(Value::Null) => saw_null = true,
Ok(other) => {
return Err(EvalError::TypeMismatch {
detail: format!(
"ANY/ALL comparison didn't return Bool: {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
Err(e) => return Err(e),
}
}
let result = if is_any {
if saw_match {
Value::Bool(true)
} else if saw_null {
Value::Null
} else {
Value::Bool(false)
}
} else if saw_mismatch {
Value::Bool(false)
} else if saw_null {
Value::Null
} else {
Value::Bool(true)
};
Ok(result)
}
#[inline(never)]
fn eval_case_arm(
operand: &Option<alloc::boxed::Box<Expr>>,
branches: &[(Expr, Expr)],
else_branch: &Option<alloc::boxed::Box<Expr>>,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
{
let mut results: Vec<&Expr> = Vec::with_capacity(branches.len() + 1);
if let Some(e) = else_branch {
results.push(e);
}
results.extend(branches.iter().map(|(_, r)| r));
unify_branch_types_static("CASE", results, ctx)?;
}
let operand_value = match operand {
Some(o) => Some(eval_expr(o, row, ctx)?),
None => None,
};
let case_hint = branches
.iter()
.map(|(_, t)| t)
.chain(else_branch.iter().map(|b| b.as_ref()))
.find_map(coalesce_type_hint);
let branch_types: Vec<spg_storage::DataType> = branches
.iter()
.map(|(_, t)| t)
.chain(else_branch.iter().map(|b| b.as_ref()))
.filter_map(|e| crate::describe::describe_expr(e, ctx.columns).map(|s| s.ty))
.collect();
let coerce = |v: Value<'static>| -> Result<Value<'static>, EvalError> {
let v = match (&v, &case_hint) {
(Value::Text(_), Some(target)) => cast::cast_value(v, target.clone())?,
_ => v,
};
Ok(widen_to_common(v, &branch_types))
};
for (when_expr, then_expr) in branches {
let when_value = eval_expr(when_expr, row, ctx)?;
let matched = match &operand_value {
None => predicate_is_true(&when_value, "CASE/WHEN", ctx.mysql_dialect)?,
Some(op_v) => {
let (l, r) = if ctx.mysql_dialect {
match (op_v, &when_value) {
(Value::Text(x), Value::Text(y)) | (Value::BpChar(x), Value::BpChar(y)) => {
(
Value::text(spg_storage::mysql_compare_fold(x)),
Value::text(spg_storage::mysql_compare_fold(y)),
)
}
_ => (op_v.clone(), when_value),
}
} else {
(op_v.clone(), when_value)
};
matches!(
apply_binary(spg_sql::ast::BinOp::Eq, l, r)?,
Value::Bool(true)
)
}
};
if matched {
return coerce(eval_expr(then_expr, row, ctx)?);
}
}
match else_branch {
Some(e) => coerce(eval_expr(e, row, ctx)?),
None => Ok(Value::Null),
}
}
#[inline(never)]
fn eval_array_slice_arm(
target: &Expr,
lo: &Option<alloc::boxed::Box<Expr>>,
hi: &Option<alloc::boxed::Box<Expr>>,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let target_v = eval_expr(target, row, ctx)?;
if matches!(target_v, Value::Null) {
return Ok(Value::Null);
}
let bound = |e: Option<&Expr>| -> Result<Option<i64>, EvalError> {
match e {
None => Ok(None),
Some(b) => match eval_expr(b, row, ctx)? {
Value::Null => Ok(None),
Value::Int(n) => Ok(Some(i64::from(n))),
Value::BigInt(n) => Ok(Some(n)),
Value::SmallInt(n) => Ok(Some(i64::from(n))),
other => Err(EvalError::TypeMismatch {
detail: format!(
"array slice bound must be integer, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
},
}
};
let lo_b = bound(lo.as_deref())?;
let hi_b = bound(hi.as_deref())?;
fn window(len: usize, lo: Option<i64>, hi: Option<i64>) -> (usize, usize) {
let start = lo.map_or(0, |l| (l.max(1) - 1) as usize).min(len);
let end = hi.map_or(len, |h| h.max(0) as usize).min(len);
(start, end.max(start))
}
match target_v {
Value::TextArray(items) => {
let (s, e) = window(items.len(), lo_b, hi_b);
Ok(Value::TextArray(items[s..e].to_vec()))
}
Value::IntArray(items) => {
let (s, e) = window(items.len(), lo_b, hi_b);
Ok(Value::IntArray(items[s..e].to_vec()))
}
Value::BigIntArray(items) => {
let (s, e) = window(items.len(), lo_b, hi_b);
Ok(Value::BigIntArray(items[s..e].to_vec()))
}
other => Err(EvalError::TypeMismatch {
detail: format!(
"slice target must be an array, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
#[inline(never)]
fn eval_in_list_arm(
expr: &Expr,
list: &[Expr],
negated: bool,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
require_in_list_comparable(expr, list, ctx)?;
let in_fold = ctx.mysql_dialect
&& !resolve::operand_is_binary_column(expr, ctx)
&& !resolve::is_binary_coerced(expr)
&& !list.iter().any(|i| resolve::is_binary_coerced(i));
let needle = mysql_collation_key(eval_expr(expr, row, ctx)?, in_fold);
let needle_null = matches!(needle, Value::Null);
let mut saw_null = needle_null && !list.is_empty();
let mut matched = false;
if !needle_null {
for item in list {
let v = mysql_collation_key(eval_expr(item, row, ctx)?, in_fold);
if matches!(v, Value::Null) {
saw_null = true;
continue;
}
match apply_binary(BinOp::Eq, needle.clone(), v)? {
Value::Bool(true) => {
matched = true;
break;
}
Value::Bool(false) => {}
Value::Null => saw_null = true,
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"IN comparison didn't return Bool: {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
}
}
}
let inner = if matched {
Value::Bool(true)
} else if saw_null {
Value::Null
} else {
Value::Bool(false)
};
Ok(match (negated, inner) {
(true, Value::Bool(b)) => Value::Bool(!b),
(_, v) => v,
})
}
#[inline(never)]
fn eval_like_arm(
expr: &Expr,
pattern: &Expr,
negated: bool,
case_insensitive: bool,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let v = eval_expr(expr, row, ctx)?;
let p = eval_expr(pattern, row, ctx)?;
let (text, pat) = match (v, p) {
(Value::Null, _) | (_, Value::Null) => return Ok(Value::Null),
(Value::Text(a) | Value::BpChar(a), Value::Text(b) | Value::BpChar(b)) => (a, b),
(Value::Text(_) | Value::BpChar(_), other) | (other, _) => {
return Err(EvalError::TypeMismatch {
detail: format!(
"LIKE requires text operands, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
let mysql = ctx.mysql_dialect
&& !resolve::operand_is_binary_column(expr, ctx)
&& !resolve::operand_is_binary_column(pattern, ctx)
&& !resolve::is_binary_coerced(expr)
&& !resolve::is_binary_coerced(pattern);
let m = if case_insensitive {
like_match(&text.to_lowercase(), &pat.to_lowercase())?
} else if mysql {
like_match(
&spg_storage::mysql_ci_fold(&text),
&spg_storage::mysql_ci_fold(&pat),
)?
} else {
like_match(&text, &pat)?
};
Ok(Value::Bool(if negated { !m } else { m }))
}
#[inline(never)]
fn eval_extract_arm(
field: &spg_sql::ast::ExtractField,
source: &Expr,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let v = eval_expr(source, row, ctx)?;
extract_from_value(field, v, source, ctx)
}
pub(crate) fn extract_from_value(
field: &spg_sql::ast::ExtractField,
v: Value<'static>,
source: &Expr,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let v = match &v {
Value::Text(s) if ctx.mysql_dialect => text_as_temporal(s).unwrap_or(v),
_ => v,
};
if matches!(
field,
spg_sql::ast::ExtractField::Timezone
| spg_sql::ast::ExtractField::TimezoneHour
| spg_sql::ast::ExtractField::TimezoneMinute
) && let Value::Timestamp(t) = &v
&& crate::describe::describe_expr(source, ctx.columns)
.is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
{
let off_secs = ctx.session_tz_offset_at(*t) / 1_000_000;
let n = match field {
spg_sql::ast::ExtractField::Timezone => off_secs,
spg_sql::ast::ExtractField::TimezoneHour => off_secs / 3600,
_ => (off_secs / 60) % 60,
};
return Ok(Value::Numeric {
scaled: i128::from(n),
scale: 0,
kind: spg_storage::NumericKind::Finite,
});
}
let v = match &v {
Value::Timestamp(t)
if !matches!(
field,
spg_sql::ast::ExtractField::Epoch
| spg_sql::ast::ExtractField::Julian
| spg_sql::ast::ExtractField::Timezone
| spg_sql::ast::ExtractField::TimezoneHour
| spg_sql::ast::ExtractField::TimezoneMinute
) && crate::describe::describe_expr(source, ctx.columns)
.is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz)) =>
{
Value::Timestamp(t.saturating_add(ctx.session_tz_offset_at(*t)))
}
_ => v,
};
let static_declared = matches!(source, Expr::Cast { .. } | Expr::Column(_))
.then(|| crate::describe::describe_expr(source, ctx.columns))
.flatten()
.map(|sch| sch.ty);
let src_name = match static_declared {
Some(spg_storage::DataType::Timestamptz) => "timestamp with time zone",
_ => datetime::value_src_type_name(&v),
};
if matches!(
field,
spg_sql::ast::ExtractField::Timezone
| spg_sql::ast::ExtractField::TimezoneHour
| spg_sql::ast::ExtractField::TimezoneMinute
) && matches!(static_declared, Some(spg_storage::DataType::Timestamp))
{
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"unit \"{}\" not supported for type timestamp without time zone",
alloc::format!("{field}").to_lowercase()
),
});
}
if ctx.mysql_dialect
&& let spg_sql::ast::ExtractField::Other(name) = field
&& let Some(packed) = crate::eval::datetime::mysql_compound_extract(name, &v)
{
return Ok(packed);
}
extract_field(field, &v, src_name)
}
#[inline(never)]
fn eval_array_subscript_arm(
expr: &Expr,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let mut idx_exprs: Vec<&Expr> = Vec::new();
let mut base = expr;
while let Expr::ArraySubscript { target, index } = base {
idx_exprs.push(index);
base = target;
}
idx_exprs.reverse();
let base_v = eval_expr(base, row, ctx)?;
if matches!(
base_v,
Value::IntArray2D(_)
| Value::BigIntArray2D(_)
| Value::TextArray2D(_)
| Value::BoolArray2D(_)
) {
return eval_matrix_subscript(&base_v, &idx_exprs, row, ctx);
}
let mut cur = base_v;
for ix in idx_exprs {
cur = apply_one_subscript(cur, ix, row, ctx)?;
}
Ok(cur)
}
#[inline(never)]
fn eval_field_access_arm(
base: &Expr,
field: &str,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let v = eval_expr(base, row, ctx)?;
match v {
Value::Null => Ok(Value::Null),
Value::Composite(fields) => fields
.into_iter()
.find(|(name, _)| name == field)
.map(|(_, val)| val)
.ok_or_else(|| missing_field_error(base, field, ctx)),
_ => Err(not_a_composite_error(base, field, ctx)),
}
}
fn missing_field_error(base: &Expr, field: &str, ctx: &EvalContext<'_>) -> EvalError {
if let Some(name) = base_named_type(base, ctx)
&& ctx
.catalog
.is_some_and(|c| c.composite_types().contains_key(name))
{
return EvalError::TypeMismatch {
detail: alloc::format!("column \"{field}\" not found in data type {name}"),
};
}
if let Expr::Column(c) = base
&& c.qualifier.is_none()
{
if let Some(name) = ctx
.columns
.iter()
.find(|col| col.name.eq_ignore_ascii_case(&c.name))
.and_then(|col| col.user_composite_type.as_ref())
{
return EvalError::TypeMismatch {
detail: alloc::format!("column \"{field}\" not found in data type {name}"),
};
}
if ctx.catalog.is_some_and(|cat| cat.get(&c.name).is_some()) {
return EvalError::TypeMismatch {
detail: alloc::format!("column {}.{field} does not exist", c.name),
};
}
}
EvalError::TypeMismatch {
detail: alloc::format!("could not identify column \"{field}\" in record data type"),
}
}
fn base_named_type<'c>(base: &'c Expr, ctx: &'c EvalContext<'_>) -> Option<&'c str> {
match base {
Expr::Cast {
target: CastTarget::Named(name),
..
} => Some(name.as_str()),
Expr::Column(c) => ctx
.columns
.iter()
.find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
.and_then(|sc| {
sc.user_composite_type
.as_deref()
.or(sc.user_domain_type.as_deref())
.or(sc.user_enum_type.as_deref())
}),
_ => None,
}
}
fn not_a_composite_error(base: &Expr, field: &str, ctx: &EvalContext<'_>) -> EvalError {
if let Some(name) = base_named_type(base, ctx)
&& ctx.catalog.is_some_and(|c| {
c.enum_types().contains_key(name) || c.domain_types().contains_key(name)
})
{
return EvalError::TypeMismatch {
detail: alloc::format!(
"column notation .{field} applied to type {name}, which is not a composite type"
),
};
}
EvalError::TypeMismatch {
detail: alloc::format!("field access `.{field}` requires a composite (record) value"),
}
}
#[inline(never)]
fn eval_bool_test_arm(
expr: &Expr,
value: Option<bool>,
negated: bool,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let v = eval_expr(expr, row, ctx)?;
let hit = match (value, &v) {
(None, Value::Null) => true,
(None, Value::Bool(_)) => false,
(None, other) => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"argument of IS {}UNKNOWN must be type boolean, not type {}",
if negated { "NOT " } else { "" },
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
(Some(_), Value::Null) => false,
(Some(want), Value::Bool(b)) => *b == want,
(Some(want), other) if ctx.mysql_dialect => mysql_truthy(other) == want,
(Some(_), other) => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"argument of IS {}{} must be type boolean, not type {}",
if negated { "NOT " } else { "" },
if value == Some(true) { "TRUE" } else { "FALSE" },
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
Ok(Value::Bool(hit != negated))
}
fn eval_is_null_arm(
expr: &Expr,
negated: bool,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
if let Expr::FunctionCall { name, args } = expr
&& name.eq_ignore_ascii_case("row")
{
let mut all_null = true;
let mut all_non_null = true;
for a in args {
if matches!(eval_expr(a, row, ctx)?, Value::Null) {
all_non_null = false;
} else {
all_null = false;
}
}
return Ok(Value::Bool(if negated { all_non_null } else { all_null }));
}
let v = eval_expr(expr, row, ctx)?;
if let Value::Composite(fields) = &v {
let mut all_null = true;
let mut all_non_null = true;
for (_, f) in fields {
if matches!(f, Value::Null) {
all_non_null = false;
} else {
all_null = false;
}
}
return Ok(Value::Bool(if negated { all_non_null } else { all_null }));
}
let is_null = matches!(v, Value::Null);
Ok(Value::Bool(if negated { !is_null } else { is_null }))
}
pub fn eval_expr(
expr: &Expr,
row: &Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let sp = eval_stack_ptr();
let base = ctx.recursion_base.get();
if base == 0 {
ctx.recursion_base.set(sp);
} else if base.saturating_sub(sp) > MAX_EVAL_STACK_BYTES {
return Err(EvalError::StackDepthExceeded);
}
match expr {
Expr::AggregateOrdered { .. } => Err(EvalError::TypeMismatch {
detail: "aggregate ORDER BY is only valid inside an aggregating SELECT".into(),
}),
Expr::NamedArg { name, .. } => Err(EvalError::TypeMismatch {
detail: alloc::format!("named argument \"{name}\" is only valid in a function call"),
}),
Expr::Literal(l) => Ok(literal_to_value(l)),
Expr::Column(c) => resolve_column(c, row, ctx),
Expr::Placeholder(n) => {
let idx = usize::from(*n).saturating_sub(1);
ctx.params
.get(idx)
.cloned()
.ok_or_else(|| EvalError::PlaceholderOutOfRange {
n: *n,
bound: u16::try_from(ctx.params.len()).unwrap_or(u16::MAX),
})
}
Expr::Unary {
op: spg_sql::ast::UnOp::Not,
expr,
} if !ctx.mysql_dialect && is_unknown_string_literal(expr) => apply_unary(
spg_sql::ast::UnOp::Not,
coerce_unknown_literal_to_bool(expr)?,
),
Expr::Unary { op, expr } => {
let v = eval_expr(expr, row, ctx)?;
if ctx.mysql_dialect {
if let Some(r) = mysql_unary_arm(*op, &v) {
return r;
}
}
apply_unary(*op, v)
}
Expr::Binary { lhs, op, rhs }
if ctx.mysql_dialect && matches!(op, BinOp::And | BinOp::Or | BinOp::LogicalXor) =>
{
eval_mysql_connective(lhs, *op, rhs, row, ctx)
}
Expr::Binary { lhs, op, rhs } if matches!(op, BinOp::And | BinOp::Or) => {
eval_connective(lhs, *op, rhs, row, ctx)
}
Expr::Binary { lhs, op, rhs } => {
if matches!(
op,
BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq
) {
let lc = eval_expr_cow(lhs, row, ctx)?;
let rc = eval_expr_cow(rhs, row, ctx)?;
if matches!(lc.as_ref(), Value::Text(_)) && matches!(rc.as_ref(), Value::Text(_)) {
if let Some(r) = enum_compare_hook(*op, lhs, rhs, lc.as_ref(), rc.as_ref(), ctx)
{
return r;
}
if let Some(r) =
collate_compare_hook(*op, lhs, rhs, lc.as_ref(), rc.as_ref(), ctx)
{
return r;
}
}
let owned_path = needs_owned_compare(lc.as_ref(), rc.as_ref(), lhs, rhs, ctx);
if !owned_path {
if lc.as_ref().is_null() || rc.as_ref().is_null() {
return Ok(Value::Null);
}
return compare(*op, lc.as_ref(), rc.as_ref()).map_err(|e| {
unknown_literal_cmp_error(e, lhs, rhs, lc.as_ref(), rc.as_ref())
});
}
let (l, r) = collation_fold_for_compare(
*op,
lhs,
rhs,
lc.into_owned(),
rc.into_owned(),
ctx,
);
let probe = (is_unknown_string_literal(lhs) || is_unknown_string_literal(rhs))
.then(|| (l.clone(), r.clone()));
return apply_binary_in(*op, l, r, ctx.mysql_dialect).map_err(|e| match &probe {
Some((pl, pr)) => unknown_literal_cmp_error(e, lhs, rhs, pl, pr),
None => e,
});
}
let l = eval_expr(lhs, row, ctx)?;
let r = eval_expr(rhs, row, ctx)?;
let (l, r) = collation_fold_for_compare(*op, lhs, rhs, l, r, ctx);
if matches!(op, spg_sql::ast::BinOp::Concat)
&& ctx.render_style != format::RenderStyle::default()
{
let styled = |v: Value<'static>| -> Value<'static> {
match &v {
Value::Date(_)
| Value::Timestamp(_)
| Value::Interval { .. }
| Value::Float(_)
| Value::Real(_) => {
Value::text(values::value_to_text_styled(&v, &ctx.render_style))
}
_ => v,
}
};
let (sl, sr) = (styled(l), styled(r));
return apply_binary(*op, sl, sr);
}
apply_binary_mysql_unsigned(*op, lhs, rhs, l, r, ctx)
}
Expr::Cast { expr, target } => eval_cast_arm(expr, target, row, ctx),
Expr::FieldAccess { base, field } => eval_field_access_arm(base, field, row, ctx),
Expr::IsNull { expr, negated } => eval_is_null_arm(expr, *negated, row, ctx),
Expr::BoolTest {
expr,
value,
negated,
} => eval_bool_test_arm(expr, *value, *negated, row, ctx),
Expr::FunctionCall { name, args } => eval_function_call_arm(name, args, row, ctx),
Expr::Variadic(_) => Err(EvalError::TypeMismatch {
detail: "VARIADIC is only valid as a function-call argument".into(),
}),
Expr::Like {
expr,
pattern,
negated,
case_insensitive,
} => eval_like_arm(expr, pattern, *negated, *case_insensitive, row, ctx),
Expr::Extract { field, source } => eval_extract_arm(field, source, row, ctx),
Expr::ScalarSubquery(_)
| Expr::Exists { .. }
| Expr::InSubquery { .. }
| Expr::RowInSubquery { .. }
| Expr::RowCmpSubquery { .. } => Err(EvalError::TypeMismatch {
detail: "subquery reached row eval — engine resolver bug".into(),
}),
Expr::InList {
expr,
list,
negated,
} => eval_in_list_arm(expr, list, *negated, row, ctx),
Expr::WindowFunction { .. } => Err(EvalError::TypeMismatch {
detail: "window function reached row eval — engine rewrite bug".into(),
}),
Expr::Array(items) => eval_array_arm(items, row, ctx),
Expr::ArraySubscript { .. } => eval_array_subscript_arm(expr, row, ctx),
Expr::ArraySlice { target, lo, hi } => eval_array_slice_arm(target, lo, hi, row, ctx),
Expr::AnyAll {
expr,
op,
array,
is_any,
} => eval_any_all_arm(expr, op, array, *is_any, row, ctx),
Expr::Case {
operand,
branches,
else_branch,
} => eval_case_arm(operand, branches, else_branch, row, ctx),
}
}
fn coalesce_type_hint(e: &Expr) -> Option<CastTarget> {
match e {
Expr::Cast { target, .. } if !matches!(target, CastTarget::Text) => Some(target.clone()),
_ => None,
}
}
pub(crate) fn widen_value_to(v: Value<'static>, common: spg_storage::DataType) -> Value<'static> {
use spg_storage::DataType as DT;
if !matches!(
common,
DT::SmallInt
| DT::Int
| DT::BigInt
| DT::Numeric { .. }
| DT::Real
| DT::Float
| DT::Date
| DT::Time
| DT::Timestamp
| DT::Timestamptz
) {
return v;
}
if matches!(v, Value::Null) {
return v;
}
if v.data_type() == Some(common) {
return v;
}
if matches!(common, spg_storage::DataType::Numeric { .. })
&& matches!(v, Value::Numeric { .. } | Value::NumericBig(_))
{
return v;
}
let target = if matches!(common, spg_storage::DataType::Numeric { .. }) {
spg_storage::DataType::Numeric {
precision: 0,
scale: 0,
}
} else {
common
};
match crate::conversions::coerce_value(v.clone(), target, "", 0) {
Ok(cv) => cv,
Err(_) => v,
}
}
pub(crate) fn widen_to_common(
v: Value<'static>,
types: &[spg_storage::DataType],
) -> Value<'static> {
match crate::describe::common_type(types) {
Some(common) => widen_value_to(v, common),
None => v,
}
}
pub(crate) fn value_to_text_for_array(v: &Value, style: &format::RenderStyle) -> String {
match v {
Value::Text(s) | Value::Json(s) => s.to_string(),
Value::Int(n) => n.to_string(),
Value::BigInt(n) => n.to_string(),
Value::SmallInt(n) => n.to_string(),
Value::Bool(b) => {
if *b {
"t".into()
} else {
"f".into()
}
}
Value::Float(x) => format::format_float_styled(*x, style),
Value::Real(x) => format::format_real_styled(*x, style),
Value::Date(d) => format::format_date_styled(*d, style),
Value::Timestamp(t) => format::format_timestamp_styled(*t, style),
Value::Numeric {
scaled,
scale,
kind,
} => format_numeric_kind(*kind, *scaled, *scale),
_ => values::value_to_text_styled(v, style),
}
}
fn like_match(text: &str, pattern: &str) -> Result<bool, EvalError> {
let pat: Vec<char> = pattern.chars().collect();
like_match_str(text, &pat, 0)
}
pub(crate) fn pg_typeof_name_for_datatype(t: spg_storage::DataType) -> Option<&'static str> {
use spg_storage::DataType as D;
Some(match t {
D::SmallInt => "smallint",
D::Int => "integer",
D::BigInt => "bigint",
D::Float => "double precision",
D::Real => "real",
D::Numeric { .. } => "numeric",
D::Bool => "boolean",
D::Date => "date",
D::Time => "time without time zone",
D::Timestamp => "timestamp without time zone",
D::Timestamptz => "timestamp with time zone",
D::Name => "name",
D::Xid => "xid",
D::Xid8 => "xid8",
D::Oid => "oid",
D::OidArray => "oid[]",
D::Uuid => "uuid",
D::Interval => "interval",
D::Text => "text",
D::Multirange(k) => match k {
spg_storage::RangeKind::Int4 => "int4multirange",
spg_storage::RangeKind::Int8 => "int8multirange",
spg_storage::RangeKind::Num => "nummultirange",
spg_storage::RangeKind::Ts => "tsmultirange",
spg_storage::RangeKind::TsTz => "tstzmultirange",
spg_storage::RangeKind::Date => "datemultirange",
},
D::Varchar(_) => "character varying",
D::Char(_) => "character",
D::Json => "json",
D::Jsonb => "jsonb",
D::Bytes => "bytea",
D::Inet => "inet",
D::Cidr => "cidr",
D::Macaddr => "macaddr",
D::Macaddr8 => "macaddr8",
D::Bit(_) => "bit",
D::BitVarying(_) => "bit varying",
D::Xml => "xml",
D::Money => "money",
D::Point => "point",
D::Lseg => "lseg",
D::Path => "path",
D::PgBox => "box",
D::Polygon => "polygon",
D::Line => "line",
D::Circle => "circle",
D::TextArray => "text[]",
D::IntArray => "integer[]",
D::BigIntArray => "bigint[]",
D::SmallIntArray => "smallint[]",
D::FloatArray => "double precision[]",
D::NumericArray => "numeric[]",
D::BoolArray => "boolean[]",
D::DateArray => "date[]",
D::TimestampArray => "timestamp without time zone[]",
D::TimestamptzArray => "timestamp with time zone[]",
D::UuidArray => "uuid[]",
D::JsonArray => "json[]",
D::JsonbArray => "jsonb[]",
D::BytesArray => "bytea[]",
D::VarcharArray => "character varying[]",
D::CharArray => "\"char\"[]",
D::IntervalArray => "interval[]",
_ => return None,
})
}
pub(crate) fn like_match_str(text: &str, pat: &[char], mut pi: usize) -> Result<bool, EvalError> {
let mut t = text;
while pi < pat.len() {
match pat[pi] {
'%' => {
while pi < pat.len() && pat[pi] == '%' {
pi += 1;
}
if pi == pat.len() {
return Ok(true);
}
let mut rest = t;
loop {
if like_match_str(rest, pat, pi)? {
return Ok(true);
}
match rest.chars().next() {
Some(c) => rest = &rest[c.len_utf8()..],
None => return Ok(false),
}
}
}
'_' => match t.chars().next() {
Some(c) => {
t = &t[c.len_utf8()..];
pi += 1;
}
None => return Ok(false),
},
'\\' if pi + 1 >= pat.len() => {
if t.is_empty() {
return Ok(false);
}
return Err(EvalError::TypeMismatch {
detail: "LIKE pattern must not end with escape character".into(),
});
}
'\\' => {
let want = pat[pi + 1];
match t.chars().next() {
Some(c) if c == want => {
t = &t[c.len_utf8()..];
pi += 2;
}
_ => return Ok(false),
}
}
c => match t.chars().next() {
Some(tc) if tc == c => {
t = &t[c.len_utf8()..];
pi += 1;
}
_ => return Ok(false),
},
}
}
Ok(t.is_empty())
}
fn fn_string_to_array(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
let (text_arg, delim_arg, null_arg) = match args {
[t, d] => (t, d, None),
[t, d, n] => (t, d, Some(n)),
_ => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"string_to_array expects 2 or 3 arguments, got {}",
args.len()
),
});
}
};
let null_string: Option<&str> = match null_arg {
None | Some(Value::Null) => None,
Some(Value::Text(s)) => Some(s.as_ref()),
Some(other) => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"string_to_array null_string must be text, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
let text = match text_arg {
Value::Null => return Ok(Value::Null),
Value::Text(t) => t,
other => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"string_to_array expects text, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
if text.is_empty() {
return Ok(Value::TextArray(Vec::new()));
}
let nullify = |p: String| -> Option<String> {
if null_string == Some(p.as_str()) {
None
} else {
Some(p)
}
};
let parts: Vec<Option<String>> = match delim_arg {
Value::Null => text.chars().map(|c| nullify(c.to_string())).collect(),
Value::Text(d) if d.is_empty() => alloc::vec![nullify(text.to_string())],
Value::Text(d) => text
.split(d.as_ref())
.map(|p| nullify(p.to_string()))
.collect(),
other => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"string_to_array delimiter must be text, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
Ok(Value::TextArray(parts))
}
fn error_on_null(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
if args.len() != 1 {
return Err(EvalError::TypeMismatch {
detail: format!("error_on_null() takes 1 arg, got {}", args.len()),
});
}
if matches!(args[0], Value::Null) {
return Err(EvalError::TypeMismatch {
detail: "error_on_null(): argument is NULL".into(),
});
}
Ok(args[0].clone().into_owned())
}
fn text_arg(v: &Value) -> Result<Option<String>, EvalError> {
match v {
Value::Text(s) => Ok(Some(s.to_string())),
Value::Null => Ok(None),
other => Err(EvalError::TypeMismatch {
detail: alloc::format!(
"regex function expects TEXT arg, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
}),
}
}
const MONTH_FULL: [&str; 12] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const MONTH_ABBR: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn civil_from_days(days: i32) -> (i32, u32, u32) {
let z = i64::from(days) + 719_468;
let era = z.div_euclid(146_097);
let doe = (z - era * 146_097) as u32;
let yoe = (doe.saturating_sub(doe / 1460) + doe / 36524 - doe / 146_096) / 365;
let y_base = i64::from(yoe) + era * 400;
let doy = doe.saturating_sub(365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy.saturating_sub((153 * mp + 2) / 5) + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y_base + 1 } else { y_base };
(y as i32, m, d)
}
fn add_months_to_civil(y: i32, m: u32, d: u32, months: i32) -> (i32, u32, u32) {
let total_months = i64::from(y) * 12 + i64::from(m) - 1 + i64::from(months);
let new_year = i32::try_from(total_months.div_euclid(12)).unwrap_or(i32::MAX);
let new_month_zero = total_months.rem_euclid(12);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let new_month = (new_month_zero as u32) + 1;
let max_day = days_in_month(new_year, new_month);
(new_year, new_month, d.min(max_day))
}
const fn days_in_month(y: i32, m: u32) -> u32 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
2 => {
if y.rem_euclid(4) == 0 && (y.rem_euclid(100) != 0 || y.rem_euclid(400) == 0) {
29
} else {
28
}
}
_ => 30,
}
}
pub(crate) fn literal_to_value(l: &Literal) -> Value<'static> {
match l {
Literal::Integer(n) => {
if let Ok(small) = i32::try_from(*n) {
Value::Int(small)
} else {
Value::BigInt(*n)
}
}
Literal::Float(x) => Value::Float(*x),
Literal::Numeric { unscaled, scale } => Value::Numeric {
scaled: *unscaled,
scale: *scale,
kind: spg_storage::NumericKind::Finite,
},
Literal::NumericBig(s) => crate::conversions::big_literal_to_value(s),
Literal::String(s) => Value::text(s.clone()),
Literal::Vector(v) => Value::vector(v.clone()),
Literal::TextArray(items) => Value::TextArray(items.clone()),
Literal::IntArray(items) => Value::IntArray(items.clone()),
Literal::BigIntArray(items) => Value::BigIntArray(items.clone()),
Literal::Bool(b) => Value::Bool(*b),
Literal::Null => Value::Null,
Literal::Interval {
months,
days,
micros,
..
} => Value::Interval {
months: *months,
days: *days,
micros: *micros,
},
}
}
impl crate::Engine {
pub(crate) fn run_user_fn_query(
&self,
def: &spg_storage::FunctionDef,
stmt: &spg_sql::ast::SelectStatement,
arg_names: &[alloc::string::String],
args: &spg_storage::Row<'static>,
fn_depth: u16,
) -> Result<Value<'static>, EvalError> {
const MAX_QUERY_FN_DEPTH: u16 = 8;
if fn_depth >= MAX_QUERY_FN_DEPTH {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"function {:?}: a body with its own FROM may nest at most {MAX_QUERY_FN_DEPTH} deep",
def.name
),
});
}
let owned: alloc::vec::Vec<Value<'static>> =
args.values.iter().map(|v| v.clone().into_owned()).collect();
let bound =
bind_user_fn_args(self.active_catalog(), stmt, arg_names, &owned).map_err(|e| {
EvalError::TypeMismatch {
detail: alloc::format!("function {:?}: {e}", def.name),
}
})?;
let as_role = def.security_definer.then(|| def.owner.as_deref()).flatten();
let out = self
.exec_select_cancel_as(&bound, crate::CancelToken::none(), as_role)
.map_err(|e| EvalError::TypeMismatch {
detail: alloc::format!("function {:?}: {e}", def.name),
})?;
let crate::QueryResult::Rows { rows, .. } = out else {
return Ok(Value::Null);
};
let Some(first) = rows.first() else {
return Ok(Value::Null);
};
let v = first.values.first().cloned().unwrap_or(Value::Null);
let declared = def.returns.trim();
if declared.eq_ignore_ascii_case("VOID") {
return Ok(Value::Null);
}
crate::eval::cast::cast_value(
v.into_owned(),
spg_sql::ast::CastTarget::Named(alloc::string::String::from(declared)),
)
.or_else(|_| Ok(Value::Null))
}
}
pub(crate) fn bind_user_fn_args(
cat: &spg_storage::Catalog,
stmt: &spg_sql::ast::SelectStatement,
arg_names: &[alloc::string::String],
args: &[Value<'static>],
) -> Result<spg_sql::ast::SelectStatement, EvalError> {
let mut bound = stmt.clone();
let mut binds: alloc::collections::BTreeMap<alloc::string::String, spg_sql::ast::Expr> =
alloc::collections::BTreeMap::new();
for (i, name) in arg_names.iter().enumerate() {
if name.is_empty() {
continue;
}
let shadowed = body_from_tables(stmt).iter().any(|t| {
cat.get(t).is_some_and(|tb| {
tb.schema()
.columns
.iter()
.any(|c| c.name.eq_ignore_ascii_case(name))
})
});
if shadowed {
continue;
}
let v = args.get(i).cloned().unwrap_or(Value::Null);
let lit =
crate::substitute::value_to_literal_expr(v).map_err(|e| EvalError::TypeMismatch {
detail: alloc::format!("argument {name} cannot be bound into the body: {e}"),
})?;
binds.insert(name.to_ascii_lowercase(), lit);
}
substitute_arg_refs_in_select(&mut bound, &binds);
Ok(bound)
}
fn body_from_tables(
stmt: &spg_sql::ast::SelectStatement,
) -> alloc::vec::Vec<alloc::string::String> {
let mut out = alloc::vec::Vec::new();
if let Some(from) = &stmt.from {
out.push(from.primary.name.clone());
for j in &from.joins {
out.push(j.table.name.clone());
}
}
out
}
fn substitute_arg_refs_in_select(
stmt: &mut spg_sql::ast::SelectStatement,
binds: &alloc::collections::BTreeMap<alloc::string::String, spg_sql::ast::Expr>,
) {
use spg_sql::ast::{Expr, SelectItem};
fn walk(e: &mut Expr, binds: &alloc::collections::BTreeMap<alloc::string::String, Expr>) {
match e {
Expr::Column(c) => {
if c.qualifier.is_none()
&& let Some(lit) = binds.get(&c.name.to_ascii_lowercase())
{
*e = lit.clone();
}
}
Expr::Binary { lhs, rhs, .. } => {
walk(lhs, binds);
walk(rhs, binds);
}
Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, binds),
Expr::FunctionCall { args, .. } => args.iter_mut().for_each(|a| walk(a, binds)),
Expr::Case {
operand,
branches,
else_branch,
} => {
if let Some(o) = operand {
walk(o, binds);
}
for (c, v) in branches.iter_mut() {
walk(c, binds);
walk(v, binds);
}
if let Some(x) = else_branch {
walk(x, binds);
}
}
Expr::InList { expr, list, .. } => {
walk(expr, binds);
list.iter_mut().for_each(|it| walk(it, binds));
}
Expr::AnyAll { expr, array, .. } => {
walk(expr, binds);
walk(array, binds);
}
Expr::Array(items) => items.iter_mut().for_each(|it| walk(it, binds)),
Expr::ArraySubscript { target, index } => {
walk(target, binds);
walk(index, binds);
}
_ => {}
}
}
for item in &mut stmt.items {
if let SelectItem::Expr { expr, .. } = item {
walk(expr, binds);
}
}
if let Some(w) = &mut stmt.where_ {
walk(w, binds);
}
if let Some(h) = &mut stmt.having {
walk(h, binds);
}
if let Some(gs) = &mut stmt.group_by {
gs.iter_mut().for_each(|g| walk(g, binds));
}
for o in &mut stmt.order_by {
walk(&mut o.expr, binds);
}
if let Some(from) = &mut stmt.from {
for j in &mut from.joins {
if let Some(on) = &mut j.on {
walk(on, binds);
}
}
}
for (_, peer) in &mut stmt.unions {
substitute_arg_refs_in_select(peer, binds);
}
for cte in &mut stmt.ctes {
if let Some(s) = cte.body.as_select_mut() {
substitute_arg_refs_in_select(s, binds);
}
}
}
impl crate::Engine {
pub(crate) fn call_plpgsql_scalar_fn(
&self,
def: &spg_storage::FunctionDef,
arg_names: &[alloc::string::String],
args: &spg_storage::Row<'static>,
) -> Result<Value<'static>, EvalError> {
let block =
spg_sql::parse_function_body(def.body.trim()).map_err(|e| EvalError::TypeMismatch {
detail: alloc::format!("function {:?} body does not parse: {e}", def.name),
})?;
let mut locals: alloc::collections::BTreeMap<alloc::string::String, Value<'static>> =
alloc::collections::BTreeMap::new();
for (i, n) in arg_names.iter().enumerate() {
if n.is_empty() {
continue;
}
locals.insert(
n.to_ascii_lowercase(),
args.values.get(i).cloned().unwrap_or(Value::Null),
);
}
let dts = self
.session_param("default_text_search_config")
.map(alloc::string::String::from);
let select_into = |stmt: &spg_sql::ast::Statement| -> Result<
Value<'static>,
crate::triggers::TriggerError,
> {
let spg_sql::ast::Statement::Select(s) = stmt else {
return Err(crate::triggers::TriggerError::EvalFailed {
function: def.name.clone(),
cause: EvalError::TypeMismatch {
detail: "SELECT … INTO body must be a SELECT".into(),
},
});
};
let r = self
.exec_select_cancel(s, crate::CancelToken::none())
.map_err(|e| crate::triggers::TriggerError::EvalFailed {
function: def.name.clone(),
cause: EvalError::TypeMismatch {
detail: alloc::format!("SELECT … INTO failed: {e}"),
},
})?;
match r {
crate::QueryResult::Rows { rows, .. } => Ok(rows
.into_iter()
.next()
.and_then(|row| row.values.into_iter().next())
.unwrap_or(Value::Null)),
_ => Ok(Value::Null),
}
};
let for_query = |stmt: &spg_sql::ast::Statement| -> Result<
(
alloc::vec::Vec<alloc::string::String>,
alloc::vec::Vec<alloc::vec::Vec<Value<'static>>>,
),
crate::triggers::TriggerError,
> {
let spg_sql::ast::Statement::Select(s) = stmt else {
return Err(crate::triggers::TriggerError::EvalFailed {
function: def.name.clone(),
cause: EvalError::TypeMismatch {
detail: "FOR … IN body must be a SELECT".into(),
},
});
};
let r = self
.exec_select_cancel(s, crate::CancelToken::none())
.map_err(|e| crate::triggers::TriggerError::EvalFailed {
function: def.name.clone(),
cause: EvalError::TypeMismatch {
detail: alloc::format!("FOR … IN SELECT failed: {e}"),
},
})?;
match r {
crate::QueryResult::Rows { columns, rows } => Ok((
columns.iter().map(|c| c.name.clone()).collect(),
rows.into_iter().map(|row| row.values).collect(),
)),
_ => Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new())),
}
};
let out = crate::triggers::call_plpgsql_scalar(
&def.name,
&block,
locals,
dts.as_deref(),
Some(&select_into),
Some(&for_query),
None,
None,
)
.map_err(|e| EvalError::TypeMismatch {
detail: alloc::format!("{e}"),
})?;
let declared = def.returns.trim();
let Some(v) = out else {
if declared.eq_ignore_ascii_case("VOID") {
return Ok(Value::Null);
}
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"control reached end of function {:?} without RETURN",
def.name
),
});
};
if declared.eq_ignore_ascii_case("VOID") {
return Ok(Value::Null);
}
crate::eval::cast::cast_value(
v.into_owned(),
spg_sql::ast::CastTarget::Named(alloc::string::String::from(declared)),
)
.or_else(|_| Ok(Value::Null))
}
}
impl crate::Engine {
pub(crate) fn call_plpgsql_setof_fn(
&self,
def: &spg_storage::FunctionDef,
arg_names: &[alloc::string::String],
args: &[Value<'static>],
) -> Result<alloc::vec::Vec<alloc::vec::Vec<Value<'static>>>, EvalError> {
let block =
spg_sql::parse_function_body(def.body.trim()).map_err(|e| EvalError::TypeMismatch {
detail: alloc::format!("function {:?} body does not parse: {e}", def.name),
})?;
let mut locals: alloc::collections::BTreeMap<alloc::string::String, Value<'static>> =
alloc::collections::BTreeMap::new();
for (i, n) in arg_names.iter().enumerate() {
if n.is_empty() {
continue;
}
locals.insert(
n.to_ascii_lowercase(),
args.get(i).cloned().unwrap_or(Value::Null),
);
}
let dts = self
.session_param("default_text_search_config")
.map(alloc::string::String::from);
let run_select = |stmt: &spg_sql::ast::Statement,
what: &str|
-> Result<crate::QueryResult, crate::triggers::TriggerError> {
let spg_sql::ast::Statement::Select(s) = stmt else {
return Err(crate::triggers::TriggerError::EvalFailed {
function: def.name.clone(),
cause: EvalError::TypeMismatch {
detail: alloc::format!("{what} body must be a SELECT"),
},
});
};
self.exec_select_cancel(s, crate::CancelToken::none())
.map_err(|e| crate::triggers::TriggerError::EvalFailed {
function: def.name.clone(),
cause: EvalError::TypeMismatch {
detail: alloc::format!("{what} failed: {e}"),
},
})
};
let select_into = |stmt: &spg_sql::ast::Statement| -> Result<
Value<'static>,
crate::triggers::TriggerError,
> {
match run_select(stmt, "SELECT … INTO")? {
crate::QueryResult::Rows { rows, .. } => Ok(rows
.into_iter()
.next()
.and_then(|r| r.values.into_iter().next())
.unwrap_or(Value::Null)),
_ => Ok(Value::Null),
}
};
let for_query = |stmt: &spg_sql::ast::Statement| -> Result<
(
alloc::vec::Vec<alloc::string::String>,
alloc::vec::Vec<alloc::vec::Vec<Value<'static>>>,
),
crate::triggers::TriggerError,
> {
match run_select(stmt, "FOR … IN / RETURN QUERY")? {
crate::QueryResult::Rows { columns, rows } => Ok((
columns.iter().map(|c| c.name.clone()).collect(),
rows.into_iter().map(|r| r.values).collect(),
)),
_ => Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new())),
}
};
let sink: core::cell::RefCell<alloc::vec::Vec<alloc::vec::Vec<Value<'static>>>> =
core::cell::RefCell::new(alloc::vec::Vec::new());
crate::triggers::call_plpgsql_scalar(
&def.name,
&block,
locals,
dts.as_deref(),
Some(&select_into),
Some(&for_query),
Some(&sink),
None,
)
.map_err(|e| EvalError::TypeMismatch {
detail: alloc::format!("{e}"),
})?;
Ok(sink.into_inner())
}
}
fn unify_array_elements(
items: &[Expr],
materialised: &mut [Value<'static>],
) -> Result<(), EvalError> {
unify_construct_values("ARRAY", items, materialised)
}
fn require_in_list_comparable(
needle: &Expr,
list: &[Expr],
ctx: &EvalContext<'_>,
) -> Result<(), EvalError> {
let known_ty = |e: &Expr| {
matches!(e, Expr::Cast { .. } | Expr::Literal(_) | Expr::Column(_))
.then(|| crate::describe::describe_expr(e, ctx.columns).map(|s| s.ty))
.flatten()
};
let reg_cast = |e: &Expr| {
match e {
Expr::Cast { target, .. } => match target {
spg_sql::ast::CastTarget::RegClass | spg_sql::ast::CastTarget::RegType => true,
spg_sql::ast::CastTarget::Named(n) => {
n.eq_ignore_ascii_case("regproc")
|| n.eq_ignore_ascii_case("regnamespace")
|| n.eq_ignore_ascii_case("regtype")
|| n.eq_ignore_ascii_case("regclass")
}
_ => false,
},
_ => false,
}
};
let untyped = |e: &Expr| {
reg_cast(e)
|| matches!(
e,
Expr::Literal(spg_sql::ast::Literal::String(_))
| Expr::Literal(spg_sql::ast::Literal::Null)
)
};
if untyped(needle) {
return Ok(());
}
let Some(nt) = known_ty(needle) else {
return Ok(());
};
for item in list {
if untyped(item) {
continue;
}
let Some(it) = known_ty(item) else { continue };
if !crate::conversions::types_unify(nt, it) {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"operator does not exist: {} = {}",
crate::conversions::pg_type_name_for_error(nt),
crate::conversions::pg_type_name_for_error(it),
),
});
}
}
Ok(())
}
pub(crate) fn unify_branch_types_static<'e>(
construct: &str,
branches: impl IntoIterator<Item = &'e Expr> + Clone,
ctx: &EvalContext<'_>,
) -> Result<(), EvalError> {
use spg_storage::DataType;
let untyped = |e: &Expr| {
matches!(
e,
Expr::Literal(spg_sql::ast::Literal::String(_))
| Expr::Literal(spg_sql::ast::Literal::Null)
)
};
let mut resolved: Option<DataType> = None;
for e in branches.clone() {
if untyped(e) {
continue;
}
let known = matches!(e, Expr::Cast { .. } | Expr::Literal(_) | Expr::Column(_));
if !known {
continue;
}
let Some(ty) = crate::describe::describe_expr_type(e, ctx.columns) else {
continue;
};
match resolved {
None => resolved = Some(ty),
Some(prev) if crate::conversions::types_unify(prev, ty) => {
if matches!(prev, DataType::Int | DataType::SmallInt) {
resolved = Some(ty);
}
}
Some(prev) => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"{construct} types {} and {} cannot be matched",
crate::conversions::pg_type_name_for_error(prev),
crate::conversions::pg_type_name_for_error(ty),
),
});
}
}
}
let Some(target) = resolved else {
return Ok(());
};
if matches!(target, DataType::Text) {
return Ok(());
}
if ctx.mysql_dialect {
return Ok(());
}
for e in branches {
if !untyped(e) {
continue;
}
if let Expr::Literal(spg_sql::ast::Literal::String(lit)) = e {
crate::conversions::coerce_value(Value::text(lit.clone()), target, "", 0).map_err(
|err| match err {
crate::EngineError::Eval(ev) => ev,
other => EvalError::TypeMismatch {
detail: alloc::format!("{other}"),
},
},
)?;
}
}
Ok(())
}
pub(crate) fn unify_construct_values(
construct: &str,
items: &[Expr],
materialised: &mut [Value<'static>],
) -> Result<(), EvalError> {
use spg_storage::DataType;
let untyped = |e: &Expr| {
matches!(
e,
Expr::Literal(spg_sql::ast::Literal::String(_))
| Expr::Literal(spg_sql::ast::Literal::Null)
)
};
let mut resolved: Option<DataType> = None;
for (i, v) in materialised.iter().enumerate() {
if items.get(i).is_some_and(untyped) {
continue;
}
let Some(ty) = v.data_type() else { continue };
match resolved {
None => resolved = Some(ty),
Some(prev) if crate::conversions::types_unify(prev, ty) => {
if matches!(prev, DataType::Int | DataType::SmallInt) {
resolved = Some(ty);
}
}
Some(prev) => {
return Err(EvalError::TypeMismatch {
detail: alloc::format!(
"{construct} types {} and {} cannot be matched",
crate::conversions::pg_type_name_for_error(prev),
crate::conversions::pg_type_name_for_error(ty),
),
});
}
}
}
let Some(target) = resolved else {
return Ok(());
};
if matches!(target, DataType::Text) {
return Ok(());
}
for (i, v) in materialised.iter_mut().enumerate() {
if !items.get(i).is_some_and(untyped) || matches!(v, Value::Null) {
continue;
}
*v = crate::conversions::coerce_value(v.clone(), target, "", i).map_err(|e| match e {
crate::EngineError::Eval(ev) => ev,
other => EvalError::TypeMismatch {
detail: alloc::format!("{other}"),
},
})?;
}
Ok(())
}
fn split_trailing_zone_name(txt: &str, order: format::DateOrder) -> Option<(i64, &str)> {
if format::parse_timestamp_literal_wall_ordered(txt, order).is_some() {
return None;
}
let trimmed = txt.trim_end();
let idx = trimmed.rfind(' ')?;
let (head, tail) = (trimmed[..idx].trim(), trimmed[idx + 1..].trim());
let zone_shaped = tail.len() > 1
&& tail.bytes().any(|b| b.is_ascii_alphabetic())
&& !tail.eq_ignore_ascii_case("bc")
&& !tail.eq_ignore_ascii_case("ad");
if !zone_shaped {
return None;
}
let wall = format::parse_timestamp_literal_wall_ordered(head, order)?;
Some((wall, tail))
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use spg_sql::ast::UnOp;
use spg_storage::{ColumnSchema, DataType, Row};
fn col(name: &str, ty: DataType) -> ColumnSchema {
ColumnSchema::new(name, ty, true)
}
fn ctx<'a>(cols: &'a [ColumnSchema], alias: Option<&'a str>) -> EvalContext<'a> {
EvalContext::new(cols, alias)
}
#[test]
fn borrowed_compare_equals_owned_apply_binary() {
let vals = vec![
Value::Null,
Value::Bool(true),
Value::Bool(false),
Value::SmallInt(3),
Value::Int(3),
Value::Int(-1),
Value::BigInt(3),
Value::BigInt(100),
Value::Float(3.0),
Value::Float(2.5),
Value::text(String::new()),
Value::text("a"),
Value::text("b"),
Value::Date(10),
Value::Timestamp(1000),
Value::Numeric {
scaled: 30,
scale: 1,
kind: spg_storage::NumericKind::Finite,
},
Value::Interval {
months: 0,
days: 0,
micros: 5,
},
];
let ops = [
BinOp::Eq,
BinOp::NotEq,
BinOp::Lt,
BinOp::LtEq,
BinOp::Gt,
BinOp::GtEq,
];
let cs = vec![col("x", DataType::Int), col("y", DataType::Int)];
let c = ctx(&cs, None);
let lhs = Expr::Column(ColumnName {
qualifier: None,
name: "x".into(),
});
let rhs = Expr::Column(ColumnName {
qualifier: None,
name: "y".into(),
});
for l in &vals {
for r in &vals {
let row = Row::new(vec![l.clone(), r.clone()]);
for op in ops {
let got = eval_expr(
&Expr::Binary {
lhs: alloc::boxed::Box::new(lhs.clone()),
op,
rhs: alloc::boxed::Box::new(rhs.clone()),
},
&row,
&c,
);
let want = apply_binary(op, l.clone(), r.clone());
assert_eq!(
format!("{got:?}"),
format!("{want:?}"),
"op={op:?} l={l:?} r={r:?}"
);
}
}
}
}
fn lit(n: i64) -> Expr {
Expr::Literal(Literal::Integer(n))
}
fn null() -> Expr {
Expr::Literal(Literal::Null)
}
fn col_ref(name: &str) -> Expr {
Expr::Column(ColumnName {
qualifier: None,
name: name.into(),
})
}
#[test]
fn literal_evaluates_to_value() {
let r = Row::new(vec![]);
let cs: [ColumnSchema; 0] = [];
let c = ctx(&cs, None);
assert_eq!(eval_expr(&lit(42), &r, &c).unwrap(), Value::Int(42));
assert_eq!(
eval_expr(&Expr::Literal(Literal::Float(1.5)), &r, &c).unwrap(),
Value::Float(1.5)
);
assert_eq!(eval_expr(&null(), &r, &c).unwrap(), Value::Null);
}
#[test]
fn column_lookup_unqualified() {
let cs = vec![col("a", DataType::Int), col("b", DataType::Text)];
let r = Row::new(vec![Value::Int(7), Value::text("hi")]);
let c = ctx(&cs, None);
assert_eq!(eval_expr(&col_ref("a"), &r, &c).unwrap(), Value::Int(7));
assert_eq!(eval_expr(&col_ref("b"), &r, &c).unwrap(), Value::text("hi"));
}
#[test]
fn column_not_found_errors() {
let cs = vec![col("a", DataType::Int)];
let r = Row::new(vec![Value::Int(0)]);
let c = ctx(&cs, None);
let err = eval_expr(&col_ref("ghost"), &r, &c).unwrap_err();
assert!(matches!(err, EvalError::ColumnNotFound { ref name } if name == "ghost"));
}
#[test]
fn qualified_column_matches_alias() {
let cs = vec![col("a", DataType::Int)];
let r = Row::new(vec![Value::Int(5)]);
let c = ctx(&cs, Some("u"));
let qualified = Expr::Column(ColumnName {
qualifier: Some("u".into()),
name: "a".into(),
});
assert_eq!(eval_expr(&qualified, &r, &c).unwrap(), Value::Int(5));
}
#[test]
fn qualified_column_unknown_alias_errors() {
let cs = vec![col("a", DataType::Int)];
let r = Row::new(vec![Value::Int(5)]);
let c = ctx(&cs, Some("u"));
let wrong = Expr::Column(ColumnName {
qualifier: Some("x".into()),
name: "a".into(),
});
assert!(matches!(
eval_expr(&wrong, &r, &c).unwrap_err(),
EvalError::UnknownQualifier { .. }
));
}
#[test]
fn arithmetic_with_widening() {
let r = Row::new(vec![]);
let cs: [ColumnSchema; 0] = [];
let c = ctx(&cs, None);
let e = Expr::Binary {
lhs: alloc::boxed::Box::new(lit(2)),
op: BinOp::Add,
rhs: alloc::boxed::Box::new(Expr::Literal(Literal::Float(0.5))),
};
assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Float(2.5));
}
#[test]
fn division_by_zero_errors() {
let r = Row::new(vec![]);
let cs: [ColumnSchema; 0] = [];
let c = ctx(&cs, None);
let e = Expr::Binary {
lhs: alloc::boxed::Box::new(lit(1)),
op: BinOp::Div,
rhs: alloc::boxed::Box::new(lit(0)),
};
assert_eq!(
eval_expr(&e, &r, &c).unwrap_err(),
EvalError::DivisionByZero
);
}
#[test]
fn comparison_returns_bool() {
let r = Row::new(vec![]);
let cs: [ColumnSchema; 0] = [];
let c = ctx(&cs, None);
let e = Expr::Binary {
lhs: alloc::boxed::Box::new(lit(1)),
op: BinOp::Lt,
rhs: alloc::boxed::Box::new(lit(2)),
};
assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Bool(true));
}
#[test]
fn null_propagates_through_arithmetic() {
let r = Row::new(vec![]);
let cs: [ColumnSchema; 0] = [];
let c = ctx(&cs, None);
let e = Expr::Binary {
lhs: alloc::boxed::Box::new(lit(1)),
op: BinOp::Add,
rhs: alloc::boxed::Box::new(null()),
};
assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Null);
}
#[test]
fn stack_depth_guard_trips_on_pathological_nesting() {
let mut e = Expr::Literal(Literal::Bool(true));
for _ in 0..30_000 {
e = Expr::Binary {
lhs: alloc::boxed::Box::new(e),
op: BinOp::And,
rhs: alloc::boxed::Box::new(Expr::Literal(Literal::Bool(true))),
};
}
let r = Row::new(vec![]);
let cs: [ColumnSchema; 0] = [];
let c = ctx(&cs, None);
let err = eval_expr(&e, &r, &c).unwrap_err();
assert!(matches!(err, EvalError::StackDepthExceeded), "{err:?}");
core::mem::forget(e);
}
#[test]
fn and_three_valued_logic() {
let r = Row::new(vec![]);
let cs: [ColumnSchema; 0] = [];
let c = ctx(&cs, None);
let tt = |a: bool, b_null: bool| Expr::Binary {
lhs: alloc::boxed::Box::new(Expr::Literal(Literal::Bool(a))),
op: BinOp::And,
rhs: alloc::boxed::Box::new(if b_null {
null()
} else {
Expr::Literal(Literal::Bool(true))
}),
};
assert_eq!(
eval_expr(&tt(false, true), &r, &c).unwrap(),
Value::Bool(false)
);
assert_eq!(eval_expr(&tt(true, true), &r, &c).unwrap(), Value::Null);
assert_eq!(
eval_expr(&tt(true, false), &r, &c).unwrap(),
Value::Bool(true)
);
}
#[test]
fn or_three_valued_logic() {
let r = Row::new(vec![]);
let cs: [ColumnSchema; 0] = [];
let c = ctx(&cs, None);
let or_with_null = |a: bool| Expr::Binary {
lhs: alloc::boxed::Box::new(Expr::Literal(Literal::Bool(a))),
op: BinOp::Or,
rhs: alloc::boxed::Box::new(null()),
};
assert_eq!(
eval_expr(&or_with_null(true), &r, &c).unwrap(),
Value::Bool(true)
);
assert_eq!(
eval_expr(&or_with_null(false), &r, &c).unwrap(),
Value::Null
);
}
#[test]
fn not_on_null_is_null() {
let r = Row::new(vec![]);
let cs: [ColumnSchema; 0] = [];
let c = ctx(&cs, None);
let e = Expr::Unary {
op: UnOp::Not,
expr: alloc::boxed::Box::new(null()),
};
assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Null);
}
#[test]
fn text_comparison_lexicographic() {
let r = Row::new(vec![]);
let cs: [ColumnSchema; 0] = [];
let c = ctx(&cs, None);
let e = Expr::Binary {
lhs: alloc::boxed::Box::new(Expr::Literal(Literal::String("apple".into()))),
op: BinOp::Lt,
rhs: alloc::boxed::Box::new(Expr::Literal(Literal::String("banana".into()))),
};
assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Bool(true));
}
#[test]
fn interval_format_basics() {
assert_eq!(format_interval(0, 0, 0), "00:00:00");
assert_eq!(format_interval(0, 1, 0), "1 day");
assert_eq!(format_interval(0, -1, 0), "-1 days");
assert_eq!(format_interval(0, 0, 86_400_000_000), "24:00:00");
assert_eq!(format_interval(0, 0, 3_600_000_000), "01:00:00");
assert_eq!(format_interval(0, 1, 9_000_000), "1 day 00:00:09");
assert_eq!(format_interval(14, 0, 0), "1 year 2 mons");
assert_eq!(format_interval(-1, 0, 0), "-1 mons");
}
#[test]
fn interval_format_pg_byte_equal_day_vs_24h() {
assert_eq!(format_interval(0, 1, 0), "1 day");
assert_eq!(format_interval(0, 0, 86_400_000_000), "24:00:00");
assert_ne!(
format_interval(0, 1, 0),
format_interval(0, 0, 86_400_000_000),
);
}
}