use alloc::string::ToString;
use alloc::vec::Vec;
use spg_sql::ast::{ColumnName, Expr, Literal, OrderBy, SelectItem, SelectStatement};
use spg_storage::{ColumnSchema, Row, Value};
use crate::conversions::{
format_bigint_2d_text, format_hstore_str, format_int_2d_text, format_range_str,
format_text_2d_text,
};
use crate::eval::{self, EvalContext};
use crate::{EngineError, aggregate, value_to_order_key};
#[allow(clippy::match_same_arms)] pub(crate) fn order_by_value_cmp(
desc: bool,
nulls_first: Option<bool>,
a: &Value,
b: &Value,
) -> core::cmp::Ordering {
order_by_value_cmp_in(desc, nulls_first, a, b, false)
}
pub(crate) fn order_by_value_cmp_in(
desc: bool,
nulls_first: Option<bool>,
a: &Value,
b: &Value,
mysql: bool,
) -> core::cmp::Ordering {
order_by_value_cmp_coll(desc, nulls_first, a, b, mysql, None)
}
pub(crate) fn order_by_value_cmp_coll(
desc: bool,
nulls_first: Option<bool>,
a: &Value,
b: &Value,
mysql: bool,
collation: Option<&str>,
) -> core::cmp::Ordering {
if let (Value::Text(x), Value::Text(y), Some(c)) = (a, b, collation)
&& let Some(ord) = crate::collate::compare(c, x, y)
{
return if desc { ord.reverse() } else { ord };
}
order_by_value_cmp_raw(desc, nulls_first, a, b, mysql)
}
fn order_by_value_cmp_raw(
desc: bool,
nulls_first: Option<bool>,
a: &Value,
b: &Value,
mysql: bool,
) -> core::cmp::Ordering {
use core::cmp::Ordering;
let nf = nulls_first.unwrap_or(if mysql { !desc } else { desc });
match (matches!(a, Value::Null), matches!(b, Value::Null)) {
(true, true) => Ordering::Equal,
(true, false) => {
if nf {
Ordering::Less
} else {
Ordering::Greater
}
}
(false, true) => {
if nf {
Ordering::Greater
} else {
Ordering::Less
}
}
(false, false) => {
let c = if mysql {
if let (Value::Text(x), Value::Text(y)) = (a, b) {
spg_storage::mysql_compare_fold(x).cmp(&spg_storage::mysql_compare_fold(y))
} else {
value_cmp(a, b)
}
} else {
value_cmp(a, b)
};
if desc { c.reverse() } else { c }
}
}
}
pub(crate) fn numeric_bignum_cmp(a: &Value, b: &Value) -> Option<core::cmp::Ordering> {
use spg_storage::bignum::BigNumeric;
if !matches!(a, Value::NumericBig(_)) && !matches!(b, Value::NumericBig(_)) {
return None;
}
let to_big = |v: &Value| -> Option<BigNumeric> {
Some(match v {
Value::SmallInt(n) => BigNumeric::from_i128(i128::from(*n), 0),
Value::Int(n) => BigNumeric::from_i128(i128::from(*n), 0),
Value::BigInt(n) => BigNumeric::from_i128(i128::from(*n), 0),
Value::Numeric {
scaled,
scale,
kind,
} if *kind == spg_storage::NumericKind::Finite => {
BigNumeric::from_i128(*scaled, *scale)
}
Value::NumericBig(b) => (**b).clone(),
_ => return None,
})
};
match (to_big(a), to_big(b)) {
(Some(x), Some(y)) => Some(x.cmp(&y)),
_ => None,
}
}
pub(crate) fn value_cmp(a: &Value, b: &Value) -> core::cmp::Ordering {
use core::cmp::Ordering;
match (a, b) {
(Value::Int(x), Value::Int(y)) => return x.cmp(y),
(Value::BigInt(x), Value::BigInt(y)) => return x.cmp(y),
(Value::SmallInt(x), Value::SmallInt(y)) => return x.cmp(y),
(Value::Text(x), Value::Text(y)) => return x.cmp(y),
(Value::Bool(x), Value::Bool(y)) => return x.cmp(y),
_ => {}
}
if let Some(ord) = numeric_bignum_cmp(a, b) {
return ord;
}
{
use spg_storage::NumericKind as NK;
let kind = |v: &Value| -> Option<NK> {
match v {
Value::Numeric { kind, .. } => Some(*kind),
Value::Int(_) | Value::BigInt(_) | Value::SmallInt(_) => Some(NK::Finite),
Value::Float(x) => Some(if x.is_nan() {
NK::NaN
} else if *x == f64::INFINITY {
NK::PosInf
} else if *x == f64::NEG_INFINITY {
NK::NegInf
} else {
NK::Finite
}),
Value::Real(x) => Some(if x.is_nan() {
NK::NaN
} else if *x == f32::INFINITY {
NK::PosInf
} else if *x == f32::NEG_INFINITY {
NK::NegInf
} else {
NK::Finite
}),
_ => None,
}
};
if let (Some(lk), Some(rk)) = (kind(a), kind(b)) {
if lk != NK::Finite || rk != NK::Finite {
let rank = |k: NK| match k {
NK::NegInf => -2,
NK::Finite => 0,
NK::PosInf => 1,
NK::NaN => 2,
};
return rank(lk).cmp(&rank(rk));
}
}
}
match (a, b) {
(Value::Null, Value::Null) => Ordering::Equal,
(Value::Null, _) => Ordering::Less,
(_, Value::Null) => Ordering::Greater,
(Value::Int(x), Value::Int(y)) => x.cmp(y),
(Value::BigInt(x), Value::BigInt(y)) => x.cmp(y),
(Value::SmallInt(x), Value::SmallInt(y)) => x.cmp(y),
(Value::SmallInt(x), Value::Int(y)) => i32::from(*x).cmp(y),
(Value::Int(x), Value::SmallInt(y)) => x.cmp(&i32::from(*y)),
(Value::SmallInt(x), Value::BigInt(y)) => i64::from(*x).cmp(y),
(Value::BigInt(x), Value::SmallInt(y)) => x.cmp(&i64::from(*y)),
(Value::Int(x), Value::BigInt(y)) => i64::from(*x).cmp(y),
(Value::BigInt(x), Value::Int(y)) => x.cmp(&i64::from(*y)),
(Value::Text(x), Value::Text(y)) => x.cmp(y),
(Value::BpChar(x), Value::BpChar(y)) => {
x.trim_end_matches(' ').cmp(y.trim_end_matches(' '))
}
(Value::BpChar(x), Value::Text(y)) => x.trim_end_matches(' ').cmp(y.trim_end_matches(' ')),
(Value::Text(x), Value::BpChar(y)) => x.trim_end_matches(' ').cmp(y.trim_end_matches(' ')),
(Value::Json(x), Value::Json(y)) => match (crate::json::parse(x), crate::json::parse(y)) {
(Ok(jx), Ok(jy)) => crate::json::jsonb_compare(&jx, &jy),
_ => x.cmp(y),
},
(Value::Bool(x), Value::Bool(y)) => x.cmp(y),
(Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
(Value::Real(x), Value::Real(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
(Value::Real(x), Value::Float(y)) => {
f64::from(*x).partial_cmp(y).unwrap_or(Ordering::Equal)
}
(Value::Float(x), Value::Real(y)) => {
x.partial_cmp(&f64::from(*y)).unwrap_or(Ordering::Equal)
}
(Value::SmallInt(n), Value::Real(x)) => f64::from(*n)
.partial_cmp(&f64::from(*x))
.unwrap_or(Ordering::Equal),
(Value::Real(x), Value::SmallInt(n)) => f64::from(*x)
.partial_cmp(&f64::from(*n))
.unwrap_or(Ordering::Equal),
(Value::Int(n), Value::Real(x)) => f64::from(*n)
.partial_cmp(&f64::from(*x))
.unwrap_or(Ordering::Equal),
(Value::Real(x), Value::Int(n)) => f64::from(*x)
.partial_cmp(&f64::from(*n))
.unwrap_or(Ordering::Equal),
#[allow(clippy::cast_precision_loss)]
(Value::BigInt(n), Value::Real(x)) => (*n as f64)
.partial_cmp(&f64::from(*x))
.unwrap_or(Ordering::Equal),
#[allow(clippy::cast_precision_loss)]
(Value::Real(x), Value::BigInt(n)) => f64::from(*x)
.partial_cmp(&(*n as f64))
.unwrap_or(Ordering::Equal),
(
Value::Numeric {
scaled: xs,
scale: xsc,
..
},
Value::Real(y),
) => numeric_to_f64(*xs, *xsc)
.partial_cmp(&f64::from(*y))
.unwrap_or(Ordering::Equal),
(
Value::Real(x),
Value::Numeric {
scaled: ys,
scale: ysc,
..
},
) => f64::from(*x)
.partial_cmp(&numeric_to_f64(*ys, *ysc))
.unwrap_or(Ordering::Equal),
(Value::SmallInt(n), Value::Float(x)) => {
f64::from(*n).partial_cmp(x).unwrap_or(Ordering::Equal)
}
(Value::Float(x), Value::SmallInt(n)) => {
x.partial_cmp(&f64::from(*n)).unwrap_or(Ordering::Equal)
}
(Value::Int(n), Value::Float(x)) => f64::from(*n).partial_cmp(x).unwrap_or(Ordering::Equal),
(Value::Float(x), Value::Int(n)) => {
x.partial_cmp(&f64::from(*n)).unwrap_or(Ordering::Equal)
}
#[allow(clippy::cast_precision_loss)]
(Value::BigInt(n), Value::Float(x)) => {
(*n as f64).partial_cmp(x).unwrap_or(Ordering::Equal)
}
#[allow(clippy::cast_precision_loss)]
(Value::Float(x), Value::BigInt(n)) => {
x.partial_cmp(&(*n as f64)).unwrap_or(Ordering::Equal)
}
(
Value::Numeric {
scaled: xs,
scale: xsc,
..
},
Value::Numeric {
scaled: ys,
scale: ysc,
..
},
) => cmp_numeric(*xs, *xsc, *ys, *ysc),
(
Value::Numeric {
scaled: xs,
scale: xsc,
..
},
Value::SmallInt(y),
) => cmp_numeric(*xs, *xsc, i128::from(*y), 0),
(
Value::SmallInt(x),
Value::Numeric {
scaled: ys,
scale: ysc,
..
},
) => cmp_numeric(i128::from(*x), 0, *ys, *ysc),
(
Value::Numeric {
scaled: xs,
scale: xsc,
..
},
Value::Int(y),
) => cmp_numeric(*xs, *xsc, i128::from(*y), 0),
(
Value::Int(x),
Value::Numeric {
scaled: ys,
scale: ysc,
..
},
) => cmp_numeric(i128::from(*x), 0, *ys, *ysc),
(
Value::Numeric {
scaled: xs,
scale: xsc,
..
},
Value::BigInt(y),
) => cmp_numeric(*xs, *xsc, i128::from(*y), 0),
(
Value::BigInt(x),
Value::Numeric {
scaled: ys,
scale: ysc,
..
},
) => cmp_numeric(i128::from(*x), 0, *ys, *ysc),
(
Value::Numeric {
scaled: xs,
scale: xsc,
..
},
Value::Float(y),
) => numeric_to_f64(*xs, *xsc)
.partial_cmp(y)
.unwrap_or(Ordering::Equal),
(
Value::Float(x),
Value::Numeric {
scaled: ys,
scale: ysc,
..
},
) => x
.partial_cmp(&numeric_to_f64(*ys, *ysc))
.unwrap_or(Ordering::Equal),
(Value::Date(x), Value::Date(y)) => x.cmp(y),
(Value::Timestamp(x), Value::Timestamp(y)) => x.cmp(y),
(Value::Money(x), Value::Money(y)) => x.cmp(y),
(Value::Bytes(x), Value::Bytes(y)) => x.as_ref().cmp(y.as_ref()),
(Value::Uuid(x), Value::Uuid(y)) => x.cmp(y),
(Value::Macaddr(x), Value::Macaddr(y)) => x.cmp(y),
(Value::Macaddr8(x), Value::Macaddr8(y)) => x.cmp(y),
(
Value::Inet {
family: xf,
bits: xb,
addr: xa,
},
Value::Inet {
family: yf,
bits: yb,
addr: ya,
},
)
| (
Value::Cidr {
family: xf,
bits: xb,
addr: xa,
},
Value::Cidr {
family: yf,
bits: yb,
addr: ya,
},
) => (xf, xa, xb).cmp(&(yf, ya, yb)),
(Value::Time(x), Value::Time(y)) => x.cmp(y),
(Value::PgLsn(x), Value::PgLsn(y)) => x.cmp(y),
(Value::Char1(x), Value::Char1(y)) => x.cmp(y),
(Value::Tid(b1, o1), Value::Tid(b2, o2)) => b1.cmp(b2).then(o1.cmp(o2)),
(Value::Xid(x), Value::Xid(y)) => x.cmp(y),
(Value::Cid(x), Value::Cid(y)) => x.cmp(y),
(Value::RegClass(x, _), Value::RegClass(y, _)) => x.cmp(y),
(Value::RegClass(x, _), Value::BigInt(y)) => x.cmp(y),
(Value::BigInt(x), Value::RegClass(y, _)) => x.cmp(y),
(
Value::RegProc(x, _) | Value::RegType(x, _),
Value::RegProc(y, _) | Value::RegType(y, _),
) => x.cmp(y),
(Value::RegProc(x, _) | Value::RegType(x, _), Value::BigInt(y)) => x.cmp(y),
(Value::BigInt(x), Value::RegProc(y, _) | Value::RegType(y, _)) => x.cmp(y),
(
Value::Interval {
months: xm,
days: xd,
micros: xu,
},
Value::Interval {
months: ym,
days: yd,
micros: yu,
},
) => {
let span = |m: i32, d: i32, u: i64| -> i128 {
(i128::from(m) * 30 + i128::from(d)) * 86_400_000_000 + i128::from(u)
};
span(*xm, *xd, *xu).cmp(&span(*ym, *yd, *yu))
}
_ => alloc::format!("{a:?}").cmp(&alloc::format!("{b:?}")),
}
}
pub(crate) fn cmp_numeric(xs: i128, xsc: u16, ys: i128, ysc: u16) -> core::cmp::Ordering {
use core::cmp::Ordering;
let max_scale = xsc.max(ysc);
let widen = |v: i128, sc: u16| -> Option<i128> {
10i128
.checked_pow(u32::from(max_scale - sc))
.and_then(|f| v.checked_mul(f))
};
match (widen(xs, xsc), widen(ys, ysc)) {
(Some(a), Some(b)) => a.cmp(&b),
_ => {
let af = xs as f64 / 10f64.powi(i32::from(xsc));
let bf = ys as f64 / 10f64.powi(i32::from(ysc));
af.partial_cmp(&bf).unwrap_or(Ordering::Equal)
}
}
}
#[allow(clippy::cast_precision_loss)]
pub(crate) fn numeric_to_f64(scaled: i128, scale: u16) -> f64 {
scaled as f64 / 10f64.powi(i32::from(scale))
}
pub(crate) fn value_to_f64(v: &Value) -> Option<f64> {
match v {
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),
Value::Float(x) => Some(*x),
Value::Real(x) => Some(f64::from(*x)),
_ => None,
}
}
pub(crate) fn sort_values_for_histogram(a: &Value, b: &Value) -> core::cmp::Ordering {
use core::cmp::Ordering;
match (a, b) {
(Value::SmallInt(a), Value::SmallInt(b)) => a.cmp(b),
(Value::Int(a), Value::Int(b)) => a.cmp(b),
(Value::BigInt(a), Value::BigInt(b)) => a.cmp(b),
(Value::SmallInt(a), Value::Int(b)) => i32::from(*a).cmp(b),
(Value::Int(a), Value::SmallInt(b)) => a.cmp(&i32::from(*b)),
(Value::Int(a), Value::BigInt(b)) => i64::from(*a).cmp(b),
(Value::BigInt(a), Value::Int(b)) => a.cmp(&i64::from(*b)),
(Value::SmallInt(a), Value::BigInt(b)) => i64::from(*a).cmp(b),
(Value::BigInt(a), Value::SmallInt(b)) => a.cmp(&i64::from(*b)),
(Value::Float(a), Value::Float(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
(Value::Text(a), Value::Text(b)) => a.cmp(b),
(Value::Json(a), Value::Json(b)) => match (crate::json::parse(a), crate::json::parse(b)) {
(Ok(ja), Ok(jb)) => crate::json::jsonb_compare(&ja, &jb),
_ => a.cmp(b),
},
(Value::Bool(a), Value::Bool(b)) => a.cmp(b),
(Value::Date(a), Value::Date(b)) => a.cmp(b),
(Value::Timestamp(a), Value::Timestamp(b)) => a.cmp(b),
(Value::SmallInt(n), Value::Float(x)) => {
(f64::from(*n)).partial_cmp(x).unwrap_or(Ordering::Equal)
}
(Value::Float(x), Value::SmallInt(n)) => {
x.partial_cmp(&f64::from(*n)).unwrap_or(Ordering::Equal)
}
(Value::Int(n), Value::Float(x)) => {
(f64::from(*n)).partial_cmp(x).unwrap_or(Ordering::Equal)
}
(Value::Float(x), Value::Int(n)) => {
x.partial_cmp(&f64::from(*n)).unwrap_or(Ordering::Equal)
}
(Value::BigInt(n), Value::Float(x)) => {
#[allow(clippy::cast_precision_loss)]
let nf = *n as f64;
nf.partial_cmp(x).unwrap_or(Ordering::Equal)
}
(Value::Float(x), Value::BigInt(n)) => {
#[allow(clippy::cast_precision_loss)]
let nf = *n as f64;
x.partial_cmp(&nf).unwrap_or(Ordering::Equal)
}
_ => canonical_value_repr(a).cmp(&canonical_value_repr(b)),
}
}
pub(crate) fn render_histogram_bounds(bounds: &[alloc::string::String]) -> alloc::string::String {
let mut out = alloc::string::String::with_capacity(bounds.len() * 8 + 2);
out.push('[');
for (i, b) in bounds.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
let needs_quote = b.contains([',', '[', ']', '"']) || b.is_empty();
if needs_quote {
out.push('"');
for ch in b.chars() {
if ch == '"' || ch == '\\' {
out.push('\\');
}
out.push(ch);
}
out.push('"');
} else {
out.push_str(b);
}
}
out.push(']');
out
}
pub(crate) fn canonical_value_repr(v: &Value) -> alloc::string::String {
match v {
Value::Null => "NULL".to_string(),
Value::SmallInt(n) => alloc::format!("{n}"),
Value::Int(n) => alloc::format!("{n}"),
Value::BigInt(n) => alloc::format!("{n}"),
Value::Float(x) => alloc::format!("{x:?}"),
Value::Text(s) | Value::Json(s) => s.to_string(),
Value::BpChar(s) => s.trim_end_matches(' ').to_string(),
Value::Bool(b) => if *b { "t" } else { "f" }.to_string(),
Value::Date(d) => eval::format_date(*d),
Value::Timestamp(t) => eval::format_timestamp(*t),
Value::Time(us) => eval::format_time(*us),
Value::Year(y) => alloc::format!("{y:04}"),
Value::TimeTz { us, offset_secs } => eval::format_timetz(*us, *offset_secs),
Value::Money(c) => eval::format_money(*c),
v @ Value::Range { .. } => format_range_str(v),
Value::Hstore(pairs) => format_hstore_str(pairs),
Value::IntArray2D(rows) => format_int_2d_text(rows),
Value::BigIntArray2D(rows) => format_bigint_2d_text(rows),
Value::TextArray2D(rows) => format_text_2d_text(rows),
Value::Interval {
months,
days,
micros,
} => eval::format_interval(*months, *days, *micros),
Value::Numeric {
scaled,
scale,
kind,
} => eval::format_numeric_kind(*kind, *scaled, *scale),
Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
alloc::format!("{v:?}")
}
_ => alloc::format!("{v:?}"),
}
}
pub(crate) fn expand_group_by_all(s: &mut SelectStatement) {
if !s.group_by_all {
for (_, peer) in &mut s.unions {
expand_group_by_all(peer);
}
return;
}
let mut groups: Vec<Expr> = Vec::new();
for item in &s.items {
if let SelectItem::Expr { expr, .. } = item
&& !aggregate::contains_aggregate(expr)
{
groups.push(expr.clone());
}
}
s.group_by = Some(groups);
s.group_by_all = false;
for (_, peer) in &mut s.unions {
expand_group_by_all(peer);
}
}
fn implicit_output_label(e: &Expr) -> Option<&str> {
match e {
Expr::Cast { expr, .. } => implicit_output_label(expr),
Expr::FunctionCall { name, .. } => Some(name.as_str()),
Expr::Column(c) => Some(c.name.as_str()),
_ => None,
}
}
#[must_use]
pub(crate) fn order_by_names_an_alias(s: &SelectStatement) -> bool {
s.order_by.iter().any(|o| match &o.expr {
Expr::Column(c) if c.qualifier.is_none() => s.items.iter().any(|it| {
matches!(
it,
SelectItem::Expr { expr, alias: Some(a) }
if a.eq_ignore_ascii_case(&c.name)
&& !crate::select::expr_contains_builtin_srf(expr)
)
}),
_ => false,
})
}
pub(crate) fn resolve_order_by_position(s: &mut SelectStatement) {
let has_unions = !s.unions.is_empty();
for order in &mut s.order_by {
match &order.expr {
Expr::Literal(Literal::Integer(n)) if *n >= 1 => {
if let Ok(idx_one_based) = usize::try_from(*n) {
let idx = idx_one_based - 1;
if idx < s.items.len()
&& let SelectItem::Expr { expr, alias } = &s.items[idx]
{
if crate::select::expr_contains_builtin_srf(expr) {
if let Some(name) = alias.clone() {
order.expr = Expr::Column(ColumnName {
qualifier: None,
name,
});
}
continue;
}
order.expr = match (has_unions, alias) {
(true, Some(a)) => Expr::Column(ColumnName {
qualifier: None,
name: a.clone(),
}),
(true, None) => continue,
_ => expr.clone(),
};
}
}
}
Expr::Column(c) if c.qualifier.is_none() => {
if has_unions {
continue;
}
let target = c.name.clone();
let mut bound = false;
for item in &s.items {
if let SelectItem::Expr {
expr,
alias: Some(a),
} = item
&& a == &target
{
if crate::select::expr_contains_builtin_srf(expr) {
bound = true;
break;
}
order.expr = expr.clone();
bound = true;
break;
}
}
if !bound {
for item in &s.items {
if let SelectItem::Expr { expr, alias: None } = item
&& !matches!(expr, Expr::Column(_))
&& implicit_output_label(expr) == Some(target.as_str())
&& !crate::select::expr_contains_builtin_srf(expr)
{
order.expr = expr.clone();
break;
}
}
}
}
_ => {}
}
}
for (_, peer) in &mut s.unions {
resolve_order_by_position(peer);
}
}
#[derive(Clone, PartialEq)]
pub(crate) enum OrderKey {
NullSmall,
NullBig,
Num(f64),
Int(i128),
Text(alloc::string::String),
Bytes(alloc::vec::Vec<u8>),
Json(crate::json::JsonValue),
Array(alloc::vec::Vec<OrderKey>),
BigNum(spg_storage::bignum::BigNumeric),
}
fn order_key_elem_cmp_in(
a: &OrderKey,
b: &OrderKey,
collation: Option<&str>,
) -> core::cmp::Ordering {
if let (OrderKey::Text(x), OrderKey::Text(y), Some(c)) = (a, b, collation)
&& let Some(ord) = crate::collate::compare(c, x, y)
{
return ord;
}
order_key_elem_cmp(a, b)
}
fn order_key_elem_cmp(a: &OrderKey, b: &OrderKey) -> core::cmp::Ordering {
use core::cmp::Ordering;
match (a, b) {
(OrderKey::NullBig, OrderKey::NullBig) | (OrderKey::NullSmall, OrderKey::NullSmall) => {
Ordering::Equal
}
(OrderKey::NullBig, _) => Ordering::Greater,
(_, OrderKey::NullBig) => Ordering::Less,
(OrderKey::NullSmall, _) => Ordering::Less,
(_, OrderKey::NullSmall) => Ordering::Greater,
(OrderKey::Num(x), OrderKey::Num(y)) => match (x.is_nan(), y.is_nan()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
(false, false) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
},
(OrderKey::Int(x), OrderKey::Int(y)) => x.cmp(y),
(OrderKey::Array(x), OrderKey::Array(y)) => {
for (ex, ey) in x.iter().zip(y.iter()) {
let c = order_key_elem_cmp(ex, ey);
if c != Ordering::Equal {
return c;
}
}
x.len().cmp(&y.len())
}
(OrderKey::Text(x), OrderKey::Text(y)) => x.cmp(y),
(OrderKey::Bytes(x), OrderKey::Bytes(y)) => x.cmp(y),
(OrderKey::Json(x), OrderKey::Json(y)) => crate::json::jsonb_compare(x, y),
(OrderKey::Json(_), OrderKey::Num(_)) => Ordering::Greater,
(OrderKey::Num(_), OrderKey::Json(_)) => Ordering::Less,
(OrderKey::Json(_), OrderKey::Int(_) | OrderKey::Text(_) | OrderKey::Bytes(_)) => {
Ordering::Greater
}
(OrderKey::Int(_) | OrderKey::Text(_) | OrderKey::Bytes(_), OrderKey::Json(_)) => {
Ordering::Less
}
#[allow(clippy::cast_precision_loss)]
(OrderKey::Int(x), OrderKey::Num(y)) => {
if y.is_nan() {
Ordering::Less
} else {
(*x as f64).partial_cmp(y).unwrap_or(Ordering::Equal)
}
}
#[allow(clippy::cast_precision_loss)]
(OrderKey::Num(x), OrderKey::Int(y)) => {
if x.is_nan() {
Ordering::Greater
} else {
x.partial_cmp(&(*y as f64)).unwrap_or(Ordering::Equal)
}
}
(OrderKey::Num(_), OrderKey::Text(_) | OrderKey::Bytes(_)) => Ordering::Less,
(OrderKey::Text(_) | OrderKey::Bytes(_), OrderKey::Num(_)) => Ordering::Greater,
(OrderKey::Int(_), OrderKey::Text(_) | OrderKey::Bytes(_)) => Ordering::Less,
(OrderKey::Text(_) | OrderKey::Bytes(_), OrderKey::Int(_)) => Ordering::Greater,
(OrderKey::Text(_), OrderKey::Bytes(_)) => Ordering::Less,
(OrderKey::Bytes(_), OrderKey::Text(_)) => Ordering::Greater,
(OrderKey::Array(_), OrderKey::Num(_)) => Ordering::Greater,
(OrderKey::Num(_), OrderKey::Array(_)) => Ordering::Less,
(OrderKey::Array(_), _) => Ordering::Greater,
(_, OrderKey::Array(_)) => Ordering::Less,
(OrderKey::BigNum(x), OrderKey::BigNum(y)) => x.cmp(y),
(OrderKey::BigNum(x), OrderKey::Num(y)) => {
if y.is_nan() || *y == f64::INFINITY {
Ordering::Less
} else if *y == f64::NEG_INFINITY {
Ordering::Greater
} else if x.parts().0 {
Ordering::Less
} else {
Ordering::Greater
}
}
(OrderKey::Num(x), OrderKey::BigNum(y)) => {
if x.is_nan() || *x == f64::INFINITY {
Ordering::Greater
} else if *x == f64::NEG_INFINITY {
Ordering::Less
} else if y.parts().0 {
Ordering::Greater
} else {
Ordering::Less
}
}
(OrderKey::BigNum(x), _) => {
if x.parts().0 {
Ordering::Less
} else {
Ordering::Greater
}
}
(_, OrderKey::BigNum(y)) => {
if y.parts().0 {
Ordering::Greater
} else {
Ordering::Less
}
}
}
}
pub(crate) fn partial_sort_tagged(
tagged: &mut Vec<(Vec<OrderKey>, Row)>,
keep: Option<usize>,
descs: &[bool],
) {
partial_sort_tagged_in(tagged, keep, descs, &[]);
}
pub(crate) fn inline_int_key(k: &OrderKey) -> Option<i128> {
match k {
OrderKey::Int(n) if *n != i128::MIN && *n != i128::MAX => Some(*n),
OrderKey::NullSmall => Some(i128::MIN),
OrderKey::NullBig => Some(i128::MAX),
_ => None,
}
}
pub(crate) fn partial_sort_tagged_in(
tagged: &mut Vec<(Vec<OrderKey>, Row)>,
keep: Option<usize>,
descs: &[bool],
collations: &[Option<alloc::string::String>],
) {
let cmp = |a: &(Vec<OrderKey>, Row), b: &(Vec<OrderKey>, Row)| {
cmp_multi_key_in(&a.0, &b.0, descs, collations)
};
match keep {
Some(k) if k < tagged.len() && k > 0 => {
let pivot = k - 1;
tagged.select_nth_unstable_by(pivot, cmp);
tagged[..k].sort_by(cmp);
tagged.truncate(k);
}
_ => {
if sort_tagged_by_inline_int_key(tagged, descs) {
return;
}
tagged.sort_by(cmp);
}
}
}
fn sort_tagged_by_inline_int_key(tagged: &mut Vec<(Vec<OrderKey>, Row)>, descs: &[bool]) -> bool {
if tagged.len() < 2 {
return true;
}
if tagged.iter().any(|(k, _)| k.len() != 1) {
return false;
}
let mut order: Vec<(i128, u32)> = Vec::with_capacity(tagged.len());
for (i, (keys, _)) in tagged.iter().enumerate() {
match inline_int_key(&keys[0]) {
Some(v) => order.push((v, i as u32)),
None => return false,
}
}
if descs.first().copied().unwrap_or(false) {
order.sort_by_key(|p| core::cmp::Reverse(p.0));
} else {
order.sort_by_key(|p| p.0);
}
let mut src: Vec<Option<(Vec<OrderKey>, Row)>> =
core::mem::take(tagged).into_iter().map(Some).collect();
tagged.reserve(src.len());
for (_, i) in order {
let taken = src[i as usize]
.take()
.expect("a permutation names each row once");
tagged.push(taken);
}
true
}
pub(crate) fn sort_by_keys(tagged: &mut [(Vec<OrderKey>, Row)], descs: &[bool]) {
sort_by_keys_in(tagged, descs, &[]);
}
pub(crate) fn sort_by_keys_in(
tagged: &mut [(Vec<OrderKey>, Row)],
descs: &[bool],
collations: &[Option<alloc::string::String>],
) {
tagged.sort_by(|a, b| cmp_multi_key_in(&a.0, &b.0, descs, collations));
}
pub(crate) fn order_by_collations(
order_by: &[spg_sql::ast::OrderBy],
ctx: &EvalContext,
) -> Result<alloc::vec::Vec<Option<alloc::string::String>>, crate::EngineError> {
for o in order_by {
if let Some(name) = &o.collation
&& !crate::collate::is_supported(name)
{
return Err(crate::EngineError::Unsupported(alloc::format!(
"collation \"{name}\" is not one this build can perform"
)));
}
}
order_by
.iter()
.map(|o| {
if let Some(name) = &o.collation {
return Ok(crate::collate::is_supported(name).then(|| name.clone()));
}
let derived = crate::collate_derive::derive(&o.expr, &|c| {
let pos = eval::find_column_pos(c, ctx)?;
ctx.columns.get(pos)?.collation_name.clone()
});
if let Some((a, b)) = derived.conflict() {
return Err(crate::EngineError::Unsupported(alloc::format!(
"collation mismatch between implicit collations \"{a}\" and \"{b}\""
)));
}
Ok(derived
.name()
.filter(|n| crate::collate::is_supported(n))
.map(alloc::string::ToString::to_string))
})
.collect()
}
pub(crate) fn topk_trim(tagged: &mut Vec<(Vec<OrderKey>, Row)>, keep: usize, descs: &[bool]) {
topk_trim_recycling(
tagged,
keep,
descs,
&mut Vec::new(),
&mut Vec::new(),
&mut None,
);
}
pub(crate) fn topk_trim_recycling<'a>(
tagged: &mut Vec<(Vec<OrderKey>, Row<'a>)>,
keep: usize,
descs: &[bool],
pool: &mut Vec<Vec<Value<'a>>>,
key_pool: &mut Vec<Vec<OrderKey>>,
boundary: &mut Option<Vec<OrderKey>>,
) {
const TRIM_FLOOR: usize = 1024;
let trigger = keep.saturating_mul(2).max(TRIM_FLOOR);
if keep > 0 && tagged.len() >= trigger {
let cmp = |a: &(Vec<OrderKey>, Row<'a>), b: &(Vec<OrderKey>, Row<'a>)| {
cmp_multi_key(&a.0, &b.0, descs)
};
tagged.select_nth_unstable_by(keep - 1, cmp);
for (mut keys, row) in tagged.drain(keep..) {
let mut v = row.values;
v.clear();
pool.push(v);
keys.clear();
key_pool.push(keys);
}
if let Some((keys, _)) = tagged.get(keep - 1) {
match boundary {
Some(b) => {
b.clear();
b.extend_from_slice(keys);
}
None => *boundary = Some(keys.clone()),
}
}
}
}
pub(crate) fn cmp_multi_key(a: &[OrderKey], b: &[OrderKey], descs: &[bool]) -> core::cmp::Ordering {
cmp_multi_key_in(a, b, descs, &[])
}
pub(crate) fn cmp_multi_key_in(
a: &[OrderKey],
b: &[OrderKey],
descs: &[bool],
collations: &[Option<alloc::string::String>],
) -> core::cmp::Ordering {
use core::cmp::Ordering;
for (i, (ka, kb)) in a.iter().zip(b.iter()).enumerate() {
let ord = order_key_elem_cmp_in(ka, kb, collations.get(i).and_then(|c| c.as_deref()));
let ord = if descs.get(i).copied().unwrap_or(false) {
ord.reverse()
} else {
ord
};
if ord != Ordering::Equal {
return ord;
}
}
Ordering::Equal
}
pub(crate) fn enum_order_ordinal(expr: &Expr, v: &Value, ctx: &EvalContext) -> Option<f64> {
let Expr::Column(c) = expr else { return None };
let Value::Text(label) = v else { return None };
let pos = eval::find_column_pos(c, ctx)?;
let col = ctx.columns.get(pos)?;
let ord = if let Some(enum_name) = col.user_enum_type.as_deref() {
ctx.catalog?
.enum_types()
.get(enum_name)?
.labels
.iter()
.position(|l| l.as_str() == label.as_ref())?
} else {
col.inline_enum_variants
.as_deref()?
.iter()
.position(|l| l.as_str() == label.as_ref())?
};
#[allow(clippy::cast_precision_loss)]
Some(ord as f64)
}
pub(crate) fn build_order_keys(
order_by: &[OrderBy],
row: &Row<'static>,
ctx: &EvalContext,
) -> Result<Vec<OrderKey>, EngineError> {
let mut keys = Vec::with_capacity(order_by.len());
build_order_keys_into(order_by, row, ctx, &mut keys)?;
Ok(keys)
}
pub(crate) fn order_by_bound_positions(
order_by: &[OrderBy],
schema_cols: &[ColumnSchema],
alias: Option<&str>,
) -> Vec<Option<usize>> {
order_by
.iter()
.map(|o| bound_column_position(&o.expr, schema_cols, alias))
.collect()
}
pub(crate) fn bound_column_position(
expr: &Expr,
schema_cols: &[ColumnSchema],
alias: Option<&str>,
) -> Option<usize> {
let Expr::Column(c) = expr else {
return None;
};
if let Some(q) = c.qualifier.as_deref()
&& !alias.is_some_and(|a| q.eq_ignore_ascii_case(a))
{
return None;
}
let mut hit = None;
for (i, s) in schema_cols.iter().enumerate() {
if s.name.eq_ignore_ascii_case(&c.name) {
if hit.is_some() {
return None;
}
hit = Some(i);
}
}
let i = hit?;
if schema_cols[i].user_composite_type.is_some() {
return None;
}
Some(i)
}
pub(crate) fn build_order_keys_into(
order_by: &[OrderBy],
row: &Row<'static>,
ctx: &EvalContext,
keys: &mut Vec<OrderKey>,
) -> Result<(), EngineError> {
build_order_keys_bound(order_by, &[], row, ctx, keys)
}
pub(crate) fn build_order_keys_bound(
order_by: &[OrderBy],
bound: &[Option<usize>],
row: &Row<'static>,
ctx: &EvalContext,
keys: &mut Vec<OrderKey>,
) -> Result<(), EngineError> {
keys.clear();
keys.reserve(order_by.len());
for (i, o) in order_by.iter().enumerate() {
let borrowed: Option<&Value<'static>> = bound
.get(i)
.copied()
.flatten()
.and_then(|p| row.values.get(p));
let owned: Value<'static>;
let v: &Value<'static> = match borrowed {
Some(v) => v,
None => {
owned = eval::eval_expr(&o.expr, row, ctx)?;
&owned
}
};
if matches!(v, Value::Null) {
let nf = o
.nulls_first
.unwrap_or(if ctx.mysql_dialect { !o.desc } else { o.desc });
keys.push(if nf == o.desc {
OrderKey::NullBig
} else {
OrderKey::NullSmall
});
} else if let Some(ord) = enum_order_ordinal(&o.expr, v, ctx) {
keys.push(OrderKey::Num(ord));
} else if ctx.mysql_dialect && matches!(v, Value::Text(_) | Value::BpChar(_)) {
let s = match v {
Value::Text(s) | Value::BpChar(s) => s.as_ref(),
_ => unreachable!("guarded by matches! above"),
};
keys.push(OrderKey::Text(spg_storage::mysql_compare_fold(s)));
} else {
keys.push(value_to_order_key(v)?);
}
}
Ok(())
}
pub(crate) fn apply_offset_and_limit(
rows: &mut Vec<Row<'static>>,
offset: Option<u32>,
limit: Option<u32>,
) {
if let Some(off) = offset {
let off = off as usize;
if off >= rows.len() {
rows.clear();
} else {
rows.drain(..off);
}
}
if let Some(n) = limit {
rows.truncate(n as usize);
}
}
pub(crate) fn apply_offset_and_limit_tagged(
tagged: &mut Vec<(Vec<OrderKey>, Row)>,
offset: Option<u32>,
limit: Option<u32>,
with_ties: bool,
) {
if let Some(off) = offset {
let off = off as usize;
if off >= tagged.len() {
tagged.clear();
} else {
tagged.drain(..off);
}
}
if let Some(n) = limit {
let n = n as usize;
if with_ties && n > 0 && n < tagged.len() {
let cutoff_key = tagged[n - 1].0.clone();
let mut end = n;
while end < tagged.len() && tagged[end].0 == cutoff_key {
end += 1;
}
tagged.truncate(end);
} else {
tagged.truncate(n);
}
}
}
pub(crate) fn check_order_by_legality(
stmt: &spg_sql::ast::SelectStatement,
) -> Result<(), crate::EngineError> {
use spg_sql::ast::{Expr, Literal, SelectItem};
let err = |m: alloc::string::String| Err(crate::EngineError::Unsupported(m));
if stmt.order_by.is_empty() {
return Ok(());
}
check_order_by_positions(stmt)?;
let is_output_column = |e: &Expr| -> bool {
if matches!(e, Expr::Literal(Literal::Integer(_))) {
return true; }
stmt.items.iter().any(|item| match item {
SelectItem::Expr { expr, alias } => {
expr == e
|| match (alias, e) {
(Some(a), Expr::Column(c)) => {
c.qualifier.is_none() && c.name.eq_ignore_ascii_case(a)
}
_ => false,
}
}
_ => matches!(e, Expr::Column(_)),
})
};
if !stmt.distinct_on.is_empty() {
let mut matched = alloc::vec![false; stmt.distinct_on.len()];
for ob in &stmt.order_by {
if matched.iter().all(|m| *m) {
break;
}
match stmt.distinct_on.iter().position(|d| d == &ob.expr) {
Some(i) => matched[i] = true,
None => {
return err(
"SELECT DISTINCT ON expressions must match initial ORDER BY expressions"
.into(),
);
}
}
}
return Ok(());
}
if stmt.distinct {
for ob in &stmt.order_by {
if !is_output_column(&ob.expr) {
return err(
"for SELECT DISTINCT, ORDER BY expressions must appear in select list".into(),
);
}
}
}
Ok(())
}
pub(crate) fn check_order_by_positions(
stmt: &spg_sql::ast::SelectStatement,
) -> Result<(), crate::EngineError> {
use spg_sql::ast::{Expr, Literal, SelectItem};
let Some(width) = stmt
.items
.iter()
.all(|i| matches!(i, SelectItem::Expr { .. }))
.then(|| stmt.items.len())
else {
return Ok(());
};
for ob in &stmt.order_by {
if let Expr::Literal(Literal::Integer(n)) = &ob.expr
&& (*n < 1 || *n as usize > width as i64 as usize)
{
return Err(crate::EngineError::Unsupported(alloc::format!(
"ORDER BY position {n} is not in select list"
)));
}
}
Ok(())
}
#[cfg(test)]
mod value_cmp_mixed_numeric_tests {
use super::value_cmp;
use core::cmp::Ordering;
use spg_storage::Value;
fn num(scaled: i128, scale: u16) -> Value<'static> {
Value::Numeric {
scaled,
scale,
kind: spg_storage::NumericKind::Finite,
}
}
#[test]
fn numeric_vs_integer_exact() {
assert_eq!(value_cmp(&num(250, 2), &Value::Int(5)), Ordering::Less);
assert_eq!(value_cmp(&Value::Int(5), &num(250, 2)), Ordering::Greater);
assert_eq!(
value_cmp(&num(1000, 0), &Value::SmallInt(9)),
Ordering::Greater
);
assert_eq!(
value_cmp(&Value::SmallInt(9), &num(1000, 0)),
Ordering::Less
);
assert_eq!(
value_cmp(&num(350, 2), &Value::BigInt(3)),
Ordering::Greater
);
assert_eq!(
value_cmp(&Value::BigInt(4), &num(350, 2)),
Ordering::Greater
);
}
#[test]
fn numeric_equals_integer_when_value_equal() {
assert_eq!(value_cmp(&num(20, 1), &Value::Int(2)), Ordering::Equal);
assert_eq!(value_cmp(&Value::Int(2), &num(20, 1)), Ordering::Equal);
assert_eq!(value_cmp(&num(2, 0), &Value::SmallInt(2)), Ordering::Equal);
}
#[test]
fn numeric_vs_float_demote() {
assert_eq!(value_cmp(&num(35, 1), &Value::Float(3.5)), Ordering::Equal);
assert_eq!(
value_cmp(&num(35, 1), &Value::Float(3.0)),
Ordering::Greater
);
assert_eq!(value_cmp(&Value::Float(1.0), &num(25, 1)), Ordering::Less);
assert_eq!(
value_cmp(&Value::Float(9.9), &num(25, 1)),
Ordering::Greater
);
}
}
#[cfg(test)]
mod inline_int_key_sort_tests {
use super::*;
use alloc::vec;
fn row(tag: i32) -> Row<'static> {
Row::new(vec![Value::Int(tag)])
}
fn tag_of(r: &Row<'static>) -> i32 {
match r.values[0] {
Value::Int(n) => n,
_ => panic!("tag column"),
}
}
fn assert_agrees(name: &str, keys: &[Vec<OrderKey>], descs: &[bool]) {
let build = || -> Vec<(Vec<OrderKey>, Row<'static>)> {
keys.iter()
.enumerate()
.map(|(i, k)| (k.clone(), row(i as i32)))
.collect()
};
let mut fast = build();
partial_sort_tagged_in(&mut fast, None, descs, &[]);
let mut general = build();
general.sort_by(|a, b| cmp_multi_key_in(&a.0, &b.0, descs, &[]));
let got: Vec<i32> = fast.iter().map(|(_, r)| tag_of(r)).collect();
let want: Vec<i32> = general.iter().map(|(_, r)| tag_of(r)).collect();
assert_eq!(got, want, "{name} (desc={descs:?})");
}
#[test]
fn the_inline_path_agrees_with_the_general_comparator() {
let int = |n: i128| vec![OrderKey::Int(n)];
let plain = vec![int(5), int(-3), int(5), int(0), int(9), int(-3), int(5)];
let with_nulls = vec![
int(7),
vec![OrderKey::NullBig],
int(-2),
vec![OrderKey::NullSmall],
int(7),
vec![OrderKey::NullBig],
vec![OrderKey::NullSmall],
];
let at_the_ends = vec![
int(3),
vec![OrderKey::Int(i128::MAX)],
int(-4),
vec![OrderKey::Int(i128::MIN)],
int(3),
];
let texts = vec![
vec![OrderKey::Text("pear".into())],
vec![OrderKey::Text("apple".into())],
vec![OrderKey::Text("apple".into())],
vec![OrderKey::Num(1.5)],
];
let two = vec![
vec![OrderKey::Int(1), OrderKey::Int(9)],
vec![OrderKey::Int(1), OrderKey::Int(2)],
vec![OrderKey::Int(0), OrderKey::Int(5)],
vec![OrderKey::Int(1), OrderKey::Int(2)],
];
for (name, keys) in [
("plain ints", &plain),
("null sentinels", &with_nulls),
("keys at the ends of the range", &at_the_ends),
("non-integer keys", &texts),
] {
assert_agrees(name, keys, &[false]);
assert_agrees(name, keys, &[true]);
}
assert_agrees("two keys", &two, &[false, false]);
assert_agrees("two keys, second descending", &two, &[false, true]);
assert_agrees("empty", &[], &[false]);
assert_agrees("one row", &[int(1)], &[false]);
assert_agrees("two rows", &[int(2), int(1)], &[false]);
}
#[test]
fn the_top_n_branch_still_keeps_the_smallest_k() {
let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = (0..20i32)
.map(|i| (vec![OrderKey::Int(i128::from((i * 7) % 20))], row(i)))
.collect();
partial_sort_tagged_in(&mut tagged, Some(3), &[false], &[]);
assert_eq!(tagged.len(), 3);
let keys: Vec<i128> = tagged
.iter()
.map(|(k, _)| match k[0] {
OrderKey::Int(n) => n,
_ => panic!("int key"),
})
.collect();
assert_eq!(keys, vec![0, 1, 2]);
}
}