use std::cmp::Ordering;
use std::collections::BTreeMap;
use anyhow::{anyhow, Result};
use lora_ast::{
BinaryOp, Expr, ProjectionItem, ShowPipeline, ShowReturn, ShowYield, SortDirection, SortItem,
UnaryOp,
};
use lora_executor::{LoraValue, Row};
use super::row_projection::{
lookup_column, project_yield_items, row_from_columns, ColumnLookupContext, NamedColumn,
};
pub(crate) fn apply_pipeline(
rows: Vec<Row>,
pipeline: &ShowPipeline,
params: &BTreeMap<String, LoraValue>,
) -> Result<Vec<Row>> {
let mut rows = apply_yield(rows, &pipeline.yield_part)?;
if let Some(where_expr) = &pipeline.where_ {
rows = filter_rows(rows, where_expr, params)?;
}
rows = apply_sort_skip_limit(
rows,
&pipeline.yield_part.order,
pipeline.yield_part.skip.as_ref(),
pipeline.yield_part.limit.as_ref(),
params,
)?;
if let Some(ret) = &pipeline.return_part {
rows = apply_return(rows, ret, params)?;
}
Ok(rows)
}
fn apply_yield(rows: Vec<Row>, y: &ShowYield) -> Result<Vec<Row>> {
project_yield_items(rows, &y.items, y.star, ColumnLookupContext::ShowPipeline)
}
fn apply_return(
rows: Vec<Row>,
ret: &ShowReturn,
params: &BTreeMap<String, LoraValue>,
) -> Result<Vec<Row>> {
let projected = project_rows(rows, &ret.items, params)?;
apply_sort_skip_limit(
projected,
&ret.order,
ret.skip.as_ref(),
ret.limit.as_ref(),
params,
)
}
fn project_rows(
rows: Vec<Row>,
items: &[ProjectionItem],
params: &BTreeMap<String, LoraValue>,
) -> Result<Vec<Row>> {
let mut out = Vec::with_capacity(rows.len());
for row in rows {
let mut columns = Vec::new();
for item in items {
match item {
ProjectionItem::Star { .. } => {
for (_, name, value) in row.iter_named() {
columns.push(NamedColumn::new(name.into_owned(), value.clone()));
}
}
ProjectionItem::Expr { expr, alias, .. } => {
let value = eval_expr(expr, &row, params)?;
let name = match alias {
Some(v) => v.name.clone(),
None => render_expr_label(expr),
};
columns.push(NamedColumn::new(name, value));
}
}
}
out.push(row_from_columns(columns));
}
Ok(out)
}
fn filter_rows(
rows: Vec<Row>,
where_expr: &Expr,
params: &BTreeMap<String, LoraValue>,
) -> Result<Vec<Row>> {
let mut out = Vec::new();
for row in rows {
let v = eval_expr(where_expr, &row, params)?;
if v.is_truthy() {
out.push(row);
}
}
Ok(out)
}
fn apply_sort_skip_limit(
mut rows: Vec<Row>,
order: &[SortItem],
skip: Option<&Expr>,
limit: Option<&Expr>,
params: &BTreeMap<String, LoraValue>,
) -> Result<Vec<Row>> {
if !order.is_empty() {
let mut keyed: Vec<(Vec<LoraValue>, Row)> = rows
.into_iter()
.map(|row| {
let keys = order
.iter()
.map(|si| eval_expr(&si.expr, &row, params))
.collect::<Result<Vec<_>>>()?;
Ok::<_, anyhow::Error>((keys, row))
})
.collect::<Result<_>>()?;
keyed.sort_by(|a, b| {
for (i, dir) in order.iter().map(|si| si.direction).enumerate() {
let ord = compare_values(&a.0[i], &b.0[i]);
if ord != Ordering::Equal {
return match dir {
SortDirection::Asc => ord,
SortDirection::Desc => ord.reverse(),
};
}
}
Ordering::Equal
});
rows = keyed.into_iter().map(|(_, r)| r).collect();
}
let skip_n = match skip {
Some(e) => eval_usize(e, params, "SKIP")?,
None => 0,
};
let limit_n = match limit {
Some(e) => Some(eval_usize(e, params, "LIMIT")?),
None => None,
};
if skip_n >= rows.len() {
return Ok(Vec::new());
}
let mut iter = rows.into_iter().skip(skip_n);
let result: Vec<Row> = if let Some(n) = limit_n {
iter.by_ref().take(n).collect()
} else {
iter.collect()
};
Ok(result)
}
fn eval_usize(expr: &Expr, params: &BTreeMap<String, LoraValue>, label: &str) -> Result<usize> {
let row = Row::new();
let v = eval_expr(expr, &row, params)?;
match v {
LoraValue::Int(n) if n >= 0 => Ok(n as usize),
other => Err(anyhow!(
"{label} must evaluate to a non-negative integer, got {other:?}"
)),
}
}
fn eval_expr(expr: &Expr, row: &Row, params: &BTreeMap<String, LoraValue>) -> Result<LoraValue> {
match expr {
Expr::Variable(v) => lookup_column(row, &v.name, ColumnLookupContext::ShowPipeline),
Expr::Integer(n, _) => Ok(LoraValue::Int(*n)),
Expr::Float(f, _) => Ok(LoraValue::Float(*f)),
Expr::String(s, _) => Ok(LoraValue::String(s.clone())),
Expr::Bool(b, _) => Ok(LoraValue::Bool(*b)),
Expr::Null(_) => Ok(LoraValue::Null),
Expr::Parameter(name, _) => params
.get(name)
.cloned()
.ok_or_else(|| anyhow!("parameter `${name}` not supplied")),
Expr::List(items, _) => {
let mut out = Vec::with_capacity(items.len());
for item in items {
out.push(eval_expr(item, row, params)?);
}
Ok(LoraValue::List(out))
}
Expr::Property {
expr: inner, key, ..
} => {
let base = eval_expr(inner, row, params)?;
Ok(match base {
LoraValue::Map(map) => map.get(key).cloned().unwrap_or(LoraValue::Null),
LoraValue::Null => LoraValue::Null,
_ => LoraValue::Null,
})
}
Expr::Unary {
op, expr: inner, ..
} => {
let v = eval_expr(inner, row, params)?;
Ok(apply_unary(*op, v))
}
Expr::Binary { lhs, op, rhs, .. } => {
match op {
BinaryOp::IsNull => {
let v = eval_expr(lhs, row, params)?;
return Ok(LoraValue::Bool(matches!(v, LoraValue::Null)));
}
BinaryOp::IsNotNull => {
let v = eval_expr(lhs, row, params)?;
return Ok(LoraValue::Bool(!matches!(v, LoraValue::Null)));
}
BinaryOp::And => {
let l = eval_expr(lhs, row, params)?;
if !l.is_truthy() {
return Ok(LoraValue::Bool(false));
}
let r = eval_expr(rhs, row, params)?;
return Ok(LoraValue::Bool(r.is_truthy()));
}
BinaryOp::Or => {
let l = eval_expr(lhs, row, params)?;
if l.is_truthy() {
return Ok(LoraValue::Bool(true));
}
let r = eval_expr(rhs, row, params)?;
return Ok(LoraValue::Bool(r.is_truthy()));
}
_ => {}
}
let l = eval_expr(lhs, row, params)?;
let r = eval_expr(rhs, row, params)?;
Ok(apply_binary(*op, l, r))
}
other => Err(anyhow!(
"expression form not supported in SHOW pipeline: {other:?}"
)),
}
}
fn apply_unary(op: UnaryOp, v: LoraValue) -> LoraValue {
match op {
UnaryOp::Not => match v {
LoraValue::Null => LoraValue::Null,
other => LoraValue::Bool(!other.is_truthy()),
},
UnaryOp::Neg => match v {
LoraValue::Int(n) => LoraValue::Int(-n),
LoraValue::Float(f) => LoraValue::Float(-f),
_ => LoraValue::Null,
},
UnaryOp::Pos => v,
}
}
fn apply_binary(op: BinaryOp, l: LoraValue, r: LoraValue) -> LoraValue {
use BinaryOp::*;
match op {
Eq => LoraValue::Bool(values_equal(&l, &r)),
Ne => LoraValue::Bool(!values_equal(&l, &r)),
Lt | Gt | Le | Ge => match compare_values(&l, &r) {
Ordering::Less => LoraValue::Bool(matches!(op, Lt | Le)),
Ordering::Greater => LoraValue::Bool(matches!(op, Gt | Ge)),
Ordering::Equal => LoraValue::Bool(matches!(op, Le | Ge)),
},
In => match r {
LoraValue::List(items) => {
LoraValue::Bool(items.iter().any(|item| values_equal(item, &l)))
}
LoraValue::Null => LoraValue::Null,
_ => LoraValue::Bool(false),
},
StartsWith => string_pred(&l, &r, |a, b| a.starts_with(b)),
EndsWith => string_pred(&l, &r, |a, b| a.ends_with(b)),
Contains => string_pred(&l, &r, |a, b| a.contains(b)),
Add | Sub | Mul | Div | Mod | Pow => numeric_op(op, l, r),
Xor => LoraValue::Bool(l.is_truthy() ^ r.is_truthy()),
And => LoraValue::Bool(l.is_truthy() && r.is_truthy()),
Or => LoraValue::Bool(l.is_truthy() || r.is_truthy()),
IsNull | IsNotNull => LoraValue::Null, RegexMatch => LoraValue::Null,
}
}
fn string_pred(l: &LoraValue, r: &LoraValue, f: impl Fn(&str, &str) -> bool) -> LoraValue {
match (l, r) {
(LoraValue::String(a), LoraValue::String(b)) => LoraValue::Bool(f(a, b)),
_ => LoraValue::Null,
}
}
fn numeric_op(op: BinaryOp, l: LoraValue, r: LoraValue) -> LoraValue {
use BinaryOp::*;
let l = l.as_f64();
let r = r.as_f64();
match (l, r) {
(Some(a), Some(b)) => LoraValue::Float(match op {
Add => a + b,
Sub => a - b,
Mul => a * b,
Div => a / b,
Mod => a % b,
Pow => a.powf(b),
_ => return LoraValue::Null,
}),
_ => LoraValue::Null,
}
}
fn values_equal(l: &LoraValue, r: &LoraValue) -> bool {
match (l, r) {
(LoraValue::Null, _) | (_, LoraValue::Null) => false,
(LoraValue::Int(a), LoraValue::Int(b)) => a == b,
(LoraValue::Float(a), LoraValue::Float(b)) => a == b,
(LoraValue::Int(a), LoraValue::Float(b)) | (LoraValue::Float(b), LoraValue::Int(a)) => {
(*a as f64) == *b
}
(LoraValue::String(a), LoraValue::String(b)) => a == b,
(LoraValue::Bool(a), LoraValue::Bool(b)) => a == b,
(LoraValue::List(a), LoraValue::List(b)) => {
a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| values_equal(x, y))
}
_ => l == r,
}
}
fn compare_values(l: &LoraValue, r: &LoraValue) -> Ordering {
match (l, r) {
(LoraValue::Null, LoraValue::Null) => Ordering::Equal,
(LoraValue::Null, _) => Ordering::Greater,
(_, LoraValue::Null) => Ordering::Less,
(LoraValue::Int(a), LoraValue::Int(b)) => a.cmp(b),
(LoraValue::Float(a), LoraValue::Float(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
(LoraValue::Int(a), LoraValue::Float(b)) => {
(*a as f64).partial_cmp(b).unwrap_or(Ordering::Equal)
}
(LoraValue::Float(a), LoraValue::Int(b)) => {
a.partial_cmp(&(*b as f64)).unwrap_or(Ordering::Equal)
}
(LoraValue::String(a), LoraValue::String(b)) => a.cmp(b),
(LoraValue::Bool(a), LoraValue::Bool(b)) => a.cmp(b),
(LoraValue::List(a), LoraValue::List(b)) => {
for (x, y) in a.iter().zip(b.iter()) {
let ord = compare_values(x, y);
if ord != Ordering::Equal {
return ord;
}
}
a.len().cmp(&b.len())
}
_ => Ordering::Equal,
}
}
fn render_expr_label(expr: &Expr) -> String {
match expr {
Expr::Variable(v) => v.name.clone(),
Expr::Property {
expr: inner, key, ..
} => {
format!("{}.{}", render_expr_label(inner), key)
}
_ => "_expr".to_string(),
}
}