use minigraf::{Minigraf, OpenOptions, QueryResult, Value};
fn db() -> Minigraf {
OpenOptions::new().open_memory().unwrap()
}
fn results(r: &QueryResult) -> &Vec<Vec<Value>> {
match r {
QueryResult::QueryResults { results, .. } => results,
_ => panic!("expected QueryResults"),
}
}
fn result_count(r: &QueryResult) -> usize {
match r {
QueryResult::QueryResults { results, .. } => results.len(),
_ => panic!("expected QueryResults"),
}
}
#[test]
fn not_absent_from_dept_as_of() {
let db = db();
db.execute(
r#"(transact [[:alice :person/name "Alice"] [:alice :person/dept "eng"]
[:bob :person/name "Bob"]])"#,
)
.unwrap();
db.execute(r#"(transact [[:charlie :person/name "Charlie"] [:charlie :person/dept "hr"]])"#)
.unwrap();
let r1 = db
.execute(
r#"(query [:find ?e
:as-of 1 :valid-at :any-valid-time
:where [?e :person/name ?_n]
(not [?e :person/dept ?_d])])"#,
)
.unwrap();
assert_eq!(result_count(&r1), 1, "as-of tx 1: only bob lacks a dept");
let r2 = db
.execute(
r#"(query [:find ?e
:as-of 2 :valid-at :any-valid-time
:where [?e :person/name ?_n]
(not [?e :person/dept ?_d])])"#,
)
.unwrap();
assert_eq!(
result_count(&r2),
1,
"as-of tx 2: still only bob lacks a dept"
);
}
#[test]
fn users_without_completed_orders_not_join_count() {
let db = db();
db.execute(
r#"(transact [[:alice :user/name "Alice"]
[:bob :user/name "Bob"]
[:charlie :user/name "Charlie"]
[:o1 :order/owner :alice] [:o1 :order/status :completed]
[:o2 :order/owner :bob] [:o2 :order/status :pending]])"#,
)
.unwrap();
let r = db
.execute(
r#"(query [:find (count ?u)
:where [?u :user/name ?_n]
(not-join [?u]
[?o :order/owner ?u]
[?o :order/status :completed])])"#,
)
.unwrap();
assert_eq!(
results(&r)[0][0],
Value::Integer(2),
"bob and charlie have no completed orders"
);
}
#[test]
fn headcount_by_dept_excluding_contractors() {
let db = db();
db.execute(
r#"(transact [[:alice :emp/dept "eng"] [:bob :emp/dept "eng"] [:carol :emp/dept "eng"]
[:dave :emp/dept "hr"] [:eve :emp/dept "hr"]
[:carol :emp/contractor true]])"#,
)
.unwrap();
let r = db
.execute(
r#"(query [:find ?dept (count ?e)
:where [?e :emp/dept ?dept]
(not [?e :emp/contractor true])])"#,
)
.unwrap();
let mut rows = results(&r).clone();
rows.sort_by_key(|row| match &row[0] {
Value::String(s) => s.clone(),
_ => String::new(),
});
assert_eq!(rows.len(), 2, "two departments");
assert_eq!(rows[0][0], Value::String("eng".into()));
assert_eq!(rows[0][1], Value::Integer(2));
assert_eq!(rows[1][0], Value::String("hr".into()));
assert_eq!(rows[1][1], Value::Integer(2));
}
#[test]
fn active_staff_by_role_valid_at() {
let db = db();
db.execute(
r#"(transact {:valid-from "2023-01-01"}
[[:alice :staff/role "eng"] [:carol :staff/role "hr"]])"#,
)
.unwrap();
db.execute(
r#"(transact {:valid-from "2023-01-01" :valid-to "2024-01-01"}
[[:bob :staff/role "eng"]])"#,
)
.unwrap();
let r_2023 = db
.execute(
r#"(query [:find ?role (count ?e)
:valid-at "2023-06-01"
:where [?e :staff/role ?role]])"#,
)
.unwrap();
let mut rows_2023 = results(&r_2023).clone();
rows_2023.sort_by_key(|row| match &row[0] {
Value::String(s) => s.clone(),
_ => String::new(),
});
assert_eq!(rows_2023.len(), 2, "two roles in 2023");
assert_eq!(rows_2023[0][0], Value::String("eng".into()));
assert_eq!(rows_2023[0][1], Value::Integer(2));
assert_eq!(rows_2023[1][0], Value::String("hr".into()));
assert_eq!(rows_2023[1][1], Value::Integer(1));
let r_2024 = db
.execute(
r#"(query [:find ?role (count ?e)
:valid-at "2024-06-01"
:where [?e :staff/role ?role]])"#,
)
.unwrap();
let mut rows_2024 = results(&r_2024).clone();
rows_2024.sort_by_key(|row| match &row[0] {
Value::String(s) => s.clone(),
_ => String::new(),
});
assert_eq!(rows_2024.len(), 2, "two roles in 2024");
assert_eq!(rows_2024[0][0], Value::String("eng".into()));
assert_eq!(rows_2024[0][1], Value::Integer(1));
assert_eq!(rows_2024[1][0], Value::String("hr".into()));
assert_eq!(rows_2024[1][1], Value::Integer(1));
}
#[test]
fn recursive_reachable_excluding_blocked() {
let db = db();
db.execute(r#"(transact [[:a :edge :b] [:b :edge :c] [:c :edge :d] [:d :blocked true]])"#)
.unwrap();
db.execute(r#"(rule [(reach ?x ?y) [?x :edge ?y]])"#)
.unwrap();
db.execute(r#"(rule [(reach ?x ?y) [?x :edge ?z] (reach ?z ?y)])"#)
.unwrap();
db.execute(r#"(rule [(accessible ?x ?y) (reach ?x ?y) (not [?y :blocked true])])"#)
.unwrap();
let r = db
.execute(r#"(query [:find (count ?y) :where (accessible :a ?y)])"#)
.unwrap();
assert_eq!(
results(&r)[0][0],
Value::Integer(2),
"b and c are reachable and not blocked"
);
}
#[test]
fn department_count_or_join_two_sources() {
let db = db();
db.execute(
r#"(transact [[:alice :emp/type :ft] [:alice :fulltime/dept "eng"]
[:bob :emp/type :pt] [:bob :parttime/dept "eng"]
[:carol :emp/type :ft] [:carol :fulltime/dept "hr"]
[:dave :emp/type :fl] [:dave :freelance/dept "eng"]])"#,
)
.unwrap();
let r = db
.execute(
r#"(query [:find ?dept (count ?e)
:where [?e :emp/type ?_t]
(or [?e :fulltime/dept ?dept]
[?e :parttime/dept ?dept])])"#,
)
.unwrap();
let mut rows = results(&r).clone();
rows.sort_by_key(|row| match &row[0] {
Value::String(s) => s.clone(),
_ => String::new(),
});
assert_eq!(rows.len(), 2, "two depts");
assert_eq!(rows[0][0], Value::String("eng".into()));
assert_eq!(rows[0][1], Value::Integer(2));
assert_eq!(rows[1][0], Value::String("hr".into()));
assert_eq!(rows[1][1], Value::Integer(1));
}
#[test]
fn salary_sum_or_conditions() {
let db = db();
db.execute(
r#"(transact [[:alice :person/salary 100] [:alice :person/senior true]
[:bob :person/salary 80] [:bob :person/remote true]
[:carol :person/salary 60]
[:dave :person/salary 120] [:dave :person/senior true]
[:dave :person/remote true]])"#,
)
.unwrap();
let r = db
.execute(
r#"(query [:find (sum ?salary)
:where [?e :person/salary ?salary]
(or [?e :person/senior true]
[?e :person/remote true])])"#,
)
.unwrap();
assert_eq!(results(&r)[0][0], Value::Integer(300));
}
#[test]
fn headcount_sequence_as_of() {
let db = db();
db.execute(r#"(transact [[:alice :emp true] [:bob :emp true]])"#)
.unwrap(); db.execute(r#"(transact [[:carol :emp true]])"#).unwrap(); db.execute(r#"(transact [[:dave :emp true] [:eve :emp true]])"#)
.unwrap();
let r1 = db
.execute(
r#"(query [:find (count ?e) :as-of 1 :valid-at :any-valid-time :where [?e :emp true]])"#,
)
.unwrap();
let r2 = db
.execute(
r#"(query [:find (count ?e) :as-of 2 :valid-at :any-valid-time :where [?e :emp true]])"#,
)
.unwrap();
let r3 = db
.execute(
r#"(query [:find (count ?e) :as-of 3 :valid-at :any-valid-time :where [?e :emp true]])"#,
)
.unwrap();
assert_eq!(results(&r1)[0][0], Value::Integer(2));
assert_eq!(results(&r2)[0][0], Value::Integer(3));
assert_eq!(results(&r3)[0][0], Value::Integer(5));
}