use vantage_expressions::{Expression, Expressive, ExpressiveEnum};
use crate::identifier::Identifier;
use crate::sum::Fx;
use crate::{AnySurrealType, Expr};
pub fn count_of(expr: impl Expressive<AnySurrealType>) -> Expr {
Fx::new("count", vec![expr.expr()]).expr()
}
pub fn count_distinct(expr: impl Expressive<AnySurrealType>) -> Expr {
let distinct = Fx::new("array::distinct", vec![expr.expr()]).expr();
Fx::new("count", vec![distinct]).expr()
}
pub fn avg(expr: impl Expressive<AnySurrealType>) -> Expr {
Fx::new("math::mean", vec![expr.expr()]).expr()
}
pub fn round(expr: impl Expressive<AnySurrealType>) -> Expr {
Fx::new("math::round", vec![expr.expr()]).expr()
}
pub fn round_to(expr: impl Expressive<AnySurrealType>, places: i64) -> Expr {
Expression::new(
format!("math::fixed({{}}, {places})"),
vec![ExpressiveEnum::Nested(expr.expr())],
)
}
pub fn coalesce(a: impl Expressive<AnySurrealType>, b: impl Expressive<AnySurrealType>) -> Expr {
Expression::new(
"{} ?? {}",
vec![
ExpressiveEnum::Nested(a.expr()),
ExpressiveEnum::Nested(b.expr()),
],
)
}
pub fn nullif(a: impl Expressive<AnySurrealType>, b: impl Expressive<AnySurrealType>) -> Expr {
Expression::new(
"IF {} = {} THEN NONE ELSE {} END",
vec![
ExpressiveEnum::Nested(a.expr()),
ExpressiveEnum::Nested(b.expr()),
ExpressiveEnum::Nested(a.expr()),
],
)
}
pub fn cast(expr: impl Expressive<AnySurrealType>, ty: &str) -> Expr {
Fx::new(format!("type::{ty}"), vec![expr.expr()]).expr()
}
pub fn date_format(expr: impl Expressive<AnySurrealType>, fmt: &str) -> Expr {
Expression::new(
"time::format({}, {})",
vec![
ExpressiveEnum::Nested(expr.expr()),
ExpressiveEnum::Scalar(AnySurrealType::from(fmt.to_string())),
],
)
}
pub fn me() -> Expr {
Expression::new("", vec![])
}
pub fn graph_out(anchor: impl Expressive<AnySurrealType>, segments: &[String]) -> Expr {
graph_walk(anchor.expr(), "->", segments)
}
pub fn graph_in(anchor: impl Expressive<AnySurrealType>, segments: &[String]) -> Expr {
graph_walk(anchor.expr(), "<-", segments)
}
fn graph_walk(anchor: Expr, arrow: &str, segments: &[String]) -> Expr {
let template = format!("{{}}{arrow}{{}}");
segments.iter().fold(anchor, |path, seg| {
Expression::new(
template.clone(),
vec![
ExpressiveEnum::Nested(path),
ExpressiveEnum::Nested(Identifier::new(seg.as_str()).expr()),
],
)
})
}
pub fn field(expr: impl Expressive<AnySurrealType>, name: &str) -> Expr {
Expression::new(
"{}.{}",
vec![
ExpressiveEnum::Nested(expr.expr()),
ExpressiveEnum::Nested(Identifier::new(name).expr()),
],
)
}
pub fn index_at(expr: impl Expressive<AnySurrealType>, n: i64) -> Expr {
Expression::new(
format!("{{}}[{n}]"),
vec![ExpressiveEnum::Nested(expr.expr())],
)
}
pub fn subquery(inner: impl Expressive<AnySurrealType>) -> Expr {
Expression::new("({})", vec![ExpressiveEnum::Nested(inner.expr())])
}
pub fn first(expr: impl Expressive<AnySurrealType>) -> Expr {
Fx::new("array::first", vec![expr.expr()]).expr()
}
pub fn len(expr: impl Expressive<AnySurrealType>) -> Expr {
Fx::new("array::len", vec![expr.expr()]).expr()
}
pub fn stddev(expr: impl Expressive<AnySurrealType>) -> Expr {
Fx::new("math::stddev", vec![expr.expr()]).expr()
}
pub fn median(expr: impl Expressive<AnySurrealType>) -> Expr {
Fx::new("math::median", vec![expr.expr()]).expr()
}
pub fn lower(expr: impl Expressive<AnySurrealType>) -> Expr {
Fx::new("string::lowercase", vec![expr.expr()]).expr()
}
pub fn words(expr: impl Expressive<AnySurrealType>) -> Expr {
Fx::new("string::words", vec![expr.expr()]).expr()
}
pub fn object_entries(expr: impl Expressive<AnySurrealType>) -> Expr {
Fx::new("object::entries", vec![expr.expr()]).expr()
}
pub fn object_values(expr: impl Expressive<AnySurrealType>) -> Expr {
Fx::new("object::values", vec![expr.expr()]).expr()
}
pub fn time_group(expr: impl Expressive<AnySurrealType>, unit: &str) -> Expr {
Expression::new(
format!("time::group({{}}, '{unit}')"),
vec![ExpressiveEnum::Nested(expr.expr())],
)
}
pub fn similarity(expr: impl Expressive<AnySurrealType>, term: &str) -> Expr {
Expression::new(
format!("string::similarity::jaro_winkler({{}}, '{term}')"),
vec![ExpressiveEnum::Nested(expr.expr())],
)
}
pub fn recurse(path: impl Expressive<AnySurrealType>, min: i64, max: i64) -> Expr {
Expression::new(
format!("@.{{{min}..{max}}}({{}})"),
vec![ExpressiveEnum::Nested(path.expr())],
)
}
pub fn closure_param(name: &str) -> Expr {
crate::variable::Variable::new(name).expr()
}
fn closure_header(params: &[&str]) -> String {
let names = params
.iter()
.map(|p| format!("${p}"))
.collect::<Vec<_>>()
.join(", ");
format!("|{names}|")
}
pub fn array_map(
this: impl Expressive<AnySurrealType>,
params: &[&str],
body: impl Expressive<AnySurrealType>,
) -> Expr {
let header = closure_header(params);
Expression::new(
format!("{{}}.map({header} {{}})"),
vec![
ExpressiveEnum::Nested(this.expr()),
ExpressiveEnum::Nested(body.expr()),
],
)
}
pub fn array_fold(
this: impl Expressive<AnySurrealType>,
init: impl Expressive<AnySurrealType>,
params: &[&str],
body: impl Expressive<AnySurrealType>,
) -> Expr {
let header = closure_header(params);
Expression::new(
format!("{{}}.fold({{}}, {header} {{}})"),
vec![
ExpressiveEnum::Nested(this.expr()),
ExpressiveEnum::Nested(init.expr()),
ExpressiveEnum::Nested(body.expr()),
],
)
}
pub fn array_filter(
this: impl Expressive<AnySurrealType>,
params: &[&str],
body: impl Expressive<AnySurrealType>,
) -> Expr {
let header = closure_header(params);
Expression::new(
format!("{{}}.filter({header} {{}})"),
vec![
ExpressiveEnum::Nested(this.expr()),
ExpressiveEnum::Nested(body.expr()),
],
)
}
pub fn object_literal(entries: Vec<(String, Expr)>) -> Expr {
let mut template = String::from("{ ");
let mut params = Vec::with_capacity(entries.len());
for (i, (key, value)) in entries.into_iter().enumerate() {
if i > 0 {
template.push_str(", ");
}
template.push_str(&key);
template.push_str(": {}");
params.push(ExpressiveEnum::Nested(value));
}
template.push_str(" }");
Expression::new(template, params)
}
pub fn array_literal(items: Vec<Expr>) -> Expr {
let placeholders = vec!["{}"; items.len()].join(", ");
Expression::new(
format!("[{placeholders}]"),
items.into_iter().map(ExpressiveEnum::Nested).collect(),
)
}
#[derive(Debug, Clone, Default)]
pub struct Case {
branches: Vec<(Expr, Expr)>,
otherwise: Option<Expr>,
}
impl Case {
pub fn new() -> Self {
Self::default()
}
pub fn when(
mut self,
cond: impl Expressive<AnySurrealType>,
then: impl Expressive<AnySurrealType>,
) -> Self {
self.branches.push((cond.expr(), then.expr()));
self
}
pub fn else_(mut self, value: impl Expressive<AnySurrealType>) -> Self {
self.otherwise = Some(value.expr());
self
}
}
impl Expressive<AnySurrealType> for Case {
fn expr(&self) -> Expr {
let mut template = String::new();
let mut params: Vec<ExpressiveEnum<AnySurrealType>> = Vec::new();
for (i, (cond, then)) in self.branches.iter().enumerate() {
template.push_str(if i == 0 {
"IF {} THEN {}"
} else {
" ELSE IF {} THEN {}"
});
params.push(ExpressiveEnum::Nested(cond.clone()));
params.push(ExpressiveEnum::Nested(then.clone()));
}
if let Some(other) = &self.otherwise {
template.push_str(" ELSE {}");
params.push(ExpressiveEnum::Nested(other.clone()));
}
template.push_str(" END");
Expression::new(template, params)
}
}
impl From<Case> for Expr {
fn from(c: Case) -> Self {
c.expr()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identifier::Identifier;
use crate::surreal_expr;
#[test]
fn aggregates_lower_to_surreal() {
assert_eq!(
count_of(surreal_expr!("->placed->order")).preview(),
"count(->placed->order)"
);
assert_eq!(
avg(Identifier::new("salary")).preview(),
"math::mean(salary)"
);
assert_eq!(
round(avg(Identifier::new("total"))).preview(),
"math::round(math::mean(total))"
);
assert_eq!(
round_to(avg(Identifier::new("price")), 2).preview(),
"math::fixed(math::mean(price), 2)"
);
assert_eq!(
count_distinct(Identifier::new("id")).preview(),
"count(array::distinct(id))"
);
}
#[test]
fn coalesce_and_nullif() {
assert_eq!(
coalesce(surreal_expr!("array::first(x)"), "n/a".to_string()).preview(),
r#"array::first(x) ?? "n/a""#
);
assert_eq!(
nullif(Identifier::new("qty"), 0i64).preview(),
"IF qty = 0 THEN NONE ELSE qty END"
);
}
#[test]
fn cast_and_date_format() {
assert_eq!(cast(Identifier::new("x"), "int").preview(), "type::int(x)");
assert_eq!(
date_format(Identifier::new("created_at"), "%Y-%m").preview(),
r#"time::format(created_at, "%Y-%m")"#
);
}
fn segs(names: &[&str]) -> Vec<String> {
names.iter().map(|s| s.to_string()).collect()
}
#[test]
fn graph_traversal_lowers_to_arrow_paths() {
assert_eq!(
graph_out(me(), &segs(&["reports_to", "employee"])).preview(),
"->reports_to->employee"
);
assert_eq!(
graph_in(me(), &segs(&["reports_to", "employee"])).preview(),
"<-reports_to<-employee"
);
assert_eq!(
graph_out(me(), &segs(&["reviewed"])).preview(),
"->reviewed"
);
assert_eq!(
field(graph_out(me(), &segs(&["reports_to", "employee"])), "name").preview(),
"->reports_to->employee.name"
);
}
#[test]
fn nesting_yields_mixed_direction() {
let inner = graph_out(me(), &segs(&["placed", "order"]));
assert_eq!(
graph_in(inner, &segs(&["placed", "client"])).preview(),
"->placed->order<-placed<-client"
);
}
#[test]
fn numeric_index_appends_brackets() {
assert_eq!(
index_at(surreal_expr!("(SELECT VALUE x FROM y GROUP ALL)"), 0).preview(),
"(SELECT VALUE x FROM y GROUP ALL)[0]"
);
assert_eq!(
index_at(graph_out(me(), &segs(&["placed", "order"])), 0).preview(),
"->placed->order[0]"
);
}
#[test]
fn recursion_wraps_a_path() {
let path = graph_in(me(), &segs(&["reports_to", "employee"]));
assert_eq!(
field(recurse(path, 1, 5), "name").preview(),
"@.{1..5}(<-reports_to<-employee).name"
);
}
#[test]
fn case_renders_if_then_else() {
let c = Case::new()
.when(surreal_expr!("price >= 250"), "premium".to_string())
.when(surreal_expr!("price >= 150"), "mid".to_string())
.else_("value".to_string());
assert_eq!(
c.preview(),
r#"IF price >= 250 THEN "premium" ELSE IF price >= 150 THEN "mid" ELSE "value" END"#
);
}
#[test]
fn tier2_fns_lower_to_surreal() {
let f = Identifier::new("salary");
assert_eq!(first(Identifier::new("x")).preview(), "array::first(x)");
assert_eq!(len(Identifier::new("lines")).preview(), "array::len(lines)");
assert_eq!(stddev(f.clone()).preview(), "math::stddev(salary)");
assert_eq!(median(f).preview(), "math::median(salary)");
assert_eq!(
lower(Identifier::new("name")).preview(),
"string::lowercase(name)"
);
assert_eq!(
words(Identifier::new("name")).preview(),
"string::words(name)"
);
assert_eq!(
object_entries(Identifier::new("nutrition")).preview(),
"object::entries(nutrition)"
);
assert_eq!(
object_values(Identifier::new("nutrition")).preview(),
"object::values(nutrition)"
);
}
#[test]
fn closure_literals_and_methods_lower_to_surreal() {
let l = closure_param("value");
let obj = object_literal(vec![
("product".to_string(), field(l.clone(), "product")),
("subtotal".to_string(), field(l.clone(), "price")),
]);
assert_eq!(
obj.preview(),
"{ product: $value.product, subtotal: $value.price }"
);
assert_eq!(
array_literal(vec![field(l.clone(), "a"), field(l.clone(), "b")]).preview(),
"[$value.a, $value.b]"
);
assert_eq!(
array_map(Identifier::new("lines"), &["value"], obj).preview(),
"lines.map(|$value| { product: $value.product, subtotal: $value.price })"
);
assert_eq!(
array_fold(
Identifier::new("lines"),
0i64,
&["acc", "value"],
field(l.clone(), "price")
)
.preview(),
"lines.fold(0, |$acc, $value| $value.price)"
);
assert_eq!(
array_filter(Identifier::new("lines"), &["value"], field(l, "ok")).preview(),
"lines.filter(|$value| $value.ok)"
);
}
#[test]
fn tier2_literal_tokens_are_single_quoted() {
assert_eq!(
time_group(Identifier::new("created_at"), "month").preview(),
"time::group(created_at, 'month')"
);
assert_eq!(
similarity(lower(Identifier::new("name")), "marti mcfligh").preview(),
"string::similarity::jaro_winkler(string::lowercase(name), 'marti mcfligh')"
);
}
}