use alloc::string::ToString;
use alloc::vec::Vec;
use spg_sql::ast::{Expr, Literal, OrderBy, SelectItem, SelectStatement};
use spg_storage::{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 {
use core::cmp::Ordering;
let nf = nulls_first.unwrap_or(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 = value_cmp(a, b);
if desc { c.reverse() } else { c }
}
}
}
pub(crate) fn value_cmp(a: &Value, b: &Value) -> core::cmp::Ordering {
use core::cmp::Ordering;
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::Text(x), Value::Text(y)) => 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::Date(x), Value::Date(y)) => x.cmp(y),
(Value::Timestamp(x), Value::Timestamp(y)) => x.cmp(y),
_ => alloc::format!("{a:?}").cmp(&alloc::format!("{b:?}")),
}
}
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),
_ => 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)) | (Value::Json(a), Value::Json(b)) => 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.clone(),
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, micros } => eval::format_interval(*months, *micros),
Value::Numeric { scaled, scale } => eval::format_numeric(*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);
}
}
pub(crate) fn resolve_order_by_position(s: &mut SelectStatement) {
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, .. } = &s.items[idx]
{
order.expr = expr.clone();
}
}
}
Expr::Column(c) if c.qualifier.is_none() => {
for item in &s.items {
if let SelectItem::Expr {
expr,
alias: Some(a),
} = item
&& a == &c.name
{
order.expr = expr.clone();
break;
}
}
}
_ => {}
}
}
for (_, peer) in &mut s.unions {
resolve_order_by_position(peer);
}
}
pub(crate) fn partial_sort_tagged(
tagged: &mut Vec<(Vec<f64>, Row)>,
keep: Option<usize>,
descs: &[bool],
) {
let cmp = |a: &(Vec<f64>, Row), b: &(Vec<f64>, Row)| cmp_multi_key(&a.0, &b.0, descs);
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);
}
_ => {
tagged.sort_by(cmp);
}
}
}
pub(crate) fn sort_by_keys(tagged: &mut [(Vec<f64>, Row)], descs: &[bool]) {
tagged.sort_by(|a, b| cmp_multi_key(&a.0, &b.0, descs));
}
fn cmp_multi_key(a: &[f64], b: &[f64], descs: &[bool]) -> core::cmp::Ordering {
use core::cmp::Ordering;
for (i, (ka, kb)) in a.iter().zip(b.iter()).enumerate() {
let ord = ka.partial_cmp(kb).unwrap_or(Ordering::Equal);
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 build_order_keys(
order_by: &[OrderBy],
row: &Row,
ctx: &EvalContext,
) -> Result<Vec<f64>, EngineError> {
let mut keys = Vec::with_capacity(order_by.len());
for o in order_by {
let v = eval::eval_expr(&o.expr, row, ctx)?;
if matches!(v, Value::Null) {
let nf = o.nulls_first.unwrap_or(o.desc);
keys.push(if nf == o.desc {
f64::INFINITY
} else {
f64::NEG_INFINITY
});
} else {
keys.push(value_to_order_key(&v)?);
}
}
Ok(keys)
}
pub(crate) fn apply_offset_and_limit(rows: &mut Vec<Row>, 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<f64>, 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);
}
}
}