use minigraf::db::Minigraf;
use minigraf::{QueryResult, Value};
fn setup_employees() -> Minigraf {
let db = Minigraf::in_memory().expect("in-memory db");
db.execute(concat!(
r#"(transact ["#,
r#" [:e1 :employee/name "Alice"]"#,
r#" [:e1 :employee/dept "Engineering"]"#,
r#" [:e1 :employee/salary 90000]"#,
r#" [:e2 :employee/name "Bob"]"#,
r#" [:e2 :employee/dept "Engineering"]"#,
r#" [:e2 :employee/salary 110000]"#,
r#" [:e3 :employee/name "Carol"]"#,
r#" [:e3 :employee/dept "Product"]"#,
r#" [:e3 :employee/salary 95000]"#,
r#" [:e4 :employee/name "Dave"]"#,
r#" [:e4 :employee/dept "Product"]"#,
r#" [:e4 :employee/salary 85000]"#,
r#"])"#,
))
.expect("transact employees");
db
}
fn get_results(r: QueryResult) -> Vec<Vec<Value>> {
if let QueryResult::QueryResults { results, .. } = r {
results
} else {
panic!("expected QueryResults");
}
}
#[test]
fn row_number_assigns_sequential_positions() {
let db = setup_employees();
let result = db
.execute(
r#"(query [:find ?salary (row-number :over (:order-by ?salary))
:where [?e :employee/salary ?salary]])"#,
)
.expect("query");
let rows = get_results(result);
assert_eq!(rows.len(), 4, "expected 4 rows");
let row_85k = rows.iter().find(|r| r[0] == Value::Integer(85000));
assert!(row_85k.is_some(), "salary 85000 not found");
assert_eq!(
row_85k.unwrap()[1],
Value::Integer(1),
"85000 should be row 1"
);
let row_110k = rows.iter().find(|r| r[0] == Value::Integer(110000));
assert_eq!(
row_110k.unwrap()[1],
Value::Integer(4),
"110000 should be row 4"
);
}
#[test]
fn rank_assigns_same_rank_to_ties() {
let db = Minigraf::in_memory().expect("in-memory db");
db.execute(concat!(
r#"(transact ["#,
r#" [:a :item/score 10]"#,
r#" [:b :item/score 10]"#,
r#" [:c :item/score 20]"#,
r#"])"#,
))
.expect("transact");
let result = db
.execute(
r#"(query [:find ?score (rank :over (:order-by ?score))
:where [?e :item/score ?score]])"#,
)
.expect("query");
let rows = get_results(result);
assert_eq!(rows.len(), 3);
let tied: Vec<_> = rows.iter().filter(|r| r[0] == Value::Integer(10)).collect();
assert_eq!(tied.len(), 2);
for r in &tied {
assert_eq!(r[1], Value::Integer(1), "tied scores should both be rank 1");
}
let top = rows.iter().find(|r| r[0] == Value::Integer(20)).unwrap();
assert_eq!(
top[1],
Value::Integer(3),
"score 20 should be rank 3 (gap after tie)"
);
}
#[test]
fn cumulative_sum_over_whole_result() {
let db = setup_employees();
let result = db
.execute(
r#"(query [:find ?salary (sum ?salary :over (:order-by ?salary))
:where [?e :employee/salary ?salary]])"#,
)
.expect("query");
let rows = get_results(result);
assert_eq!(rows.len(), 4);
let row_85k = rows.iter().find(|r| r[0] == Value::Integer(85000)).unwrap();
assert_eq!(row_85k[1], Value::Integer(85000));
let row_110k = rows
.iter()
.find(|r| r[0] == Value::Integer(110000))
.unwrap();
assert_eq!(row_110k[1], Value::Integer(380000));
}
#[test]
fn sum_resets_per_partition() {
let db = setup_employees();
let result = db.execute(
r#"(query [:find ?dept ?salary (sum ?salary :over (:partition-by ?dept :order-by ?salary))
:where [?e :employee/dept ?dept]
[?e :employee/salary ?salary]])"#,
).expect("query");
let rows = get_results(result);
assert_eq!(rows.len(), 4);
let eng_90k = rows
.iter()
.find(|r| r[0] == Value::String("Engineering".into()) && r[1] == Value::Integer(90000))
.unwrap();
assert_eq!(eng_90k[2], Value::Integer(90000));
let eng_110k = rows
.iter()
.find(|r| r[0] == Value::String("Engineering".into()) && r[1] == Value::Integer(110000))
.unwrap();
assert_eq!(eng_110k[2], Value::Integer(200000));
let prod_85k = rows
.iter()
.find(|r| r[0] == Value::String("Product".into()) && r[1] == Value::Integer(85000))
.unwrap();
assert_eq!(prod_85k[2], Value::Integer(85000));
let prod_95k = rows
.iter()
.find(|r| r[0] == Value::String("Product".into()) && r[1] == Value::Integer(95000))
.unwrap();
assert_eq!(prod_95k[2], Value::Integer(180000));
}
#[test]
fn running_count_over_ordered_result() {
let db = setup_employees();
let result = db
.execute(
r#"(query [:find ?salary (count ?salary :over (:order-by ?salary))
:where [?e :employee/salary ?salary]])"#,
)
.expect("query");
let rows = get_results(result);
assert_eq!(rows.len(), 4);
let row_110k = rows
.iter()
.find(|r| r[0] == Value::Integer(110000))
.unwrap();
assert_eq!(
row_110k[1],
Value::Integer(4),
"last row should have count 4"
);
}
#[test]
fn running_min_over_ordered_result() {
let db = setup_employees();
let result = db
.execute(
r#"(query [:find ?salary (min ?salary :over (:order-by ?salary))
:where [?e :employee/salary ?salary]])"#,
)
.expect("query");
let rows = get_results(result);
let row_110k = rows
.iter()
.find(|r| r[0] == Value::Integer(110000))
.unwrap();
assert_eq!(row_110k[1], Value::Integer(85000));
}
#[test]
fn running_avg_over_ordered_result() {
let db = setup_employees();
let result = db
.execute(
r#"(query [:find ?salary (avg ?salary :over (:order-by ?salary))
:where [?e :employee/salary ?salary]])"#,
)
.expect("query");
let rows = get_results(result);
assert_eq!(rows.len(), 4);
let row_110k = rows
.iter()
.find(|r| r[0] == Value::Integer(110000))
.unwrap();
assert_eq!(row_110k[1], Value::Float(95000.0));
}
#[test]
fn row_number_desc_ordering() {
let db = setup_employees();
let result = db
.execute(
r#"(query [:find ?salary (row-number :over (:order-by ?salary :desc))
:where [?e :employee/salary ?salary]])"#,
)
.expect("query");
let rows = get_results(result);
let row_110k = rows
.iter()
.find(|r| r[0] == Value::Integer(110000))
.unwrap();
assert_eq!(row_110k[1], Value::Integer(1));
let row_85k = rows.iter().find(|r| r[0] == Value::Integer(85000)).unwrap();
assert_eq!(row_85k[1], Value::Integer(4));
}
#[test]
fn mixed_aggregate_and_window_in_same_find() {
let db = setup_employees();
let result = db
.execute(
r#"(query [:find ?dept (count ?e) (sum ?salary :over (:order-by ?salary))
:with ?e ?salary
:where [?e :employee/dept ?dept]
[?e :employee/salary ?salary]])"#,
)
.expect("query");
let rows = get_results(result);
assert_eq!(rows.len(), 2, "expected one row per dept");
let eng = rows
.iter()
.find(|r| r[0] == Value::String("Engineering".into()))
.unwrap();
assert_eq!(eng[1], Value::Integer(2), "Engineering has 2 employees");
}
#[test]
fn single_row_result_window_equals_row_value() {
let db = Minigraf::in_memory().expect("in-memory db");
db.execute(r#"(transact [[:x :score 42]])"#)
.expect("transact");
let result = db
.execute(
r#"(query [:find ?v (sum ?v :over (:order-by ?v))
:where [?e :score ?v]])"#,
)
.expect("query");
let rows = get_results(result);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][1], Value::Integer(42));
}
#[test]
fn empty_result_no_panic() {
let db = Minigraf::in_memory().expect("in-memory db");
let result = db
.execute(
r#"(query [:find ?v (sum ?v :over (:order-by ?v))
:where [?e :score ?v]])"#,
)
.expect("query");
let rows = get_results(result);
assert_eq!(rows.len(), 0);
}
#[test]
fn lag_rejected_at_parse_time() {
let db = Minigraf::in_memory().expect("in-memory db");
let result = db.execute(r#"(query [:find (lag ?v :over (:order-by ?v)) :where [?e :x ?v]])"#);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not supported"));
}
#[test]
fn lead_rejected_at_parse_time() {
let db = Minigraf::in_memory().expect("in-memory db");
let result = db.execute(r#"(query [:find (lead ?v :over (:order-by ?v)) :where [?e :x ?v]])"#);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not supported"));
}
#[test]
fn running_max_over_ordered_result() {
let db = setup_employees();
let result = db
.execute(
r#"(query [:find ?salary (max ?salary :over (:order-by ?salary))
:where [?e :employee/salary ?salary]])"#,
)
.expect("query");
let rows = get_results(result);
let row_110k = rows
.iter()
.find(|r| r[0] == Value::Integer(110000))
.unwrap();
assert_eq!(row_110k[1], Value::Integer(110000));
}