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"),
}
}
#[test]
fn time_interval_any_point_during() {
let db = db();
db.execute(
r#"(transact {:valid-from "2022-01-01" :valid-to "2023-07-01"} [[:e1 :item/label "A"]])"#,
)
.unwrap();
db.execute(r#"(transact {:valid-from "2023-07-01"} [[:e2 :item/label "B"]])"#)
.unwrap();
db.execute(
r#"(transact {:valid-from "2015-01-01" :valid-to "2020-01-01"} [[:e3 :item/label "C"]])"#,
)
.unwrap();
let r = db
.execute(
r#"
(query [:find ?e
:any-valid-time
:where [?e :item/label _]
[?e :db/valid-from ?vf]
[?e :db/valid-to ?vt]
[(<= ?vf 1704067200000)]
[(>= ?vt 1672531200000)]])
"#,
)
.unwrap();
let rows = results(&r);
assert_eq!(rows.len(), 2, "e1 and e2 overlap [2023, 2024]; e3 does not");
}
#[test]
fn time_interval_entire_interval() {
let db = db();
db.execute(
r#"(transact {:valid-from "2020-01-01" :valid-to "2025-01-01"} [[:e1 :item/label "A"]])"#,
)
.unwrap();
db.execute(r#"(transact {:valid-from "2023-07-01"} [[:e2 :item/label "B"]])"#)
.unwrap();
let r = db
.execute(
r#"
(query [:find ?e
:any-valid-time
:where [?e :item/label _]
[?e :db/valid-from ?vf]
[?e :db/valid-to ?vt]
[(<= ?vf 1672531200000)]
[(>= ?vt 1704067200000)]])
"#,
)
.unwrap();
let rows = results(&r);
assert_eq!(rows.len(), 1, "only e1 covers the entire interval");
}
#[test]
fn time_point_lookup_salary_threshold() {
let db = db();
db.execute(r#"(transact {:valid-from "2023-01-01" :valid-to "2024-01-01"} [[:alice :person/salary 100000]])"#).unwrap();
db.execute(r#"(transact {:valid-from "2024-01-01"} [[:alice :person/salary 30000]])"#)
.unwrap();
let r = db
.execute(
r#"
(query [:find ?vf
:any-valid-time
:where [:alice :person/salary ?s]
[:alice :db/valid-from ?vf]
[(> ?s 50000)]])
"#,
)
.unwrap();
let rows = results(&r);
assert_eq!(rows.len(), 1, "only the 2023 salary entry exceeds 50000");
assert_eq!(
rows[0][0],
Value::Integer(1672531200000),
"valid-from = 2023-01-01"
);
}
#[test]
fn time_interval_lookup_employment_status() {
let db = db();
db.execute(r#"(transact {:valid-from "2022-01-01" :valid-to "2023-01-01"} [[:alice :employment/status :probation]])"#).unwrap();
db.execute(r#"(transact {:valid-from "2023-01-01" :valid-to "2025-01-01"} [[:alice :employment/status :permanent]])"#).unwrap();
let r = db
.execute(
r#"
(query [:find ?vf ?vt
:any-valid-time
:where [:alice :employment/status _]
[:alice :db/valid-from ?vf]
[:alice :db/valid-to ?vt]])
"#,
)
.unwrap();
let rows = results(&r);
assert_eq!(rows.len(), 2, "two distinct employment intervals");
}
#[test]
fn tx_count_binding() {
let db = db();
db.execute(r#"(transact [[:alice :person/name "Alice"]])"#)
.unwrap(); db.execute(r#"(transact [[:bob :person/name "Bob"]])"#)
.unwrap();
let r = db
.execute(
r#"
(query [:find ?e ?tc
:any-valid-time
:where [?e :person/name _]
[?e :db/tx-count ?tc]])
"#,
)
.unwrap();
let rows = results(&r);
assert_eq!(rows.len(), 2);
let mut counts: Vec<i64> = rows
.iter()
.map(|r| match r[1] {
Value::Integer(n) => n,
_ => panic!("expected Integer"),
})
.collect();
counts.sort();
assert_eq!(counts, vec![1, 2]);
}
#[test]
fn tx_id_same_transaction_join() {
let db = db();
db.execute(r#"(transact [[:alice :person/name "Alice"] [:bob :person/name "Bob"]])"#)
.unwrap();
let r = db
.execute(
r#"
(query [:find ?e1 ?e2
:any-valid-time
:where [?e1 :person/name _]
[?e2 :person/name _]
[?e1 :db/tx-id ?tx]
[?e2 :db/tx-id ?tx]])
"#,
)
.unwrap();
let rows = results(&r);
assert_eq!(
rows.len(),
4,
"cross-join of 2 entities with same tx-id = 4 rows"
);
}
#[test]
fn valid_at_explicit_timestamp() {
let db = db();
db.execute(r#"(transact {:valid-from "2020-01-01"} [[:alice :person/name "Alice"]])"#)
.unwrap();
let r = db
.execute(
r#"
(query [:find ?vat
:valid-at "2023-01-01"
:where [:alice :person/name _]
[:alice :db/valid-at ?vat]])
"#,
)
.unwrap();
let rows = results(&r);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0], Value::Integer(1672531200000));
}
#[test]
fn valid_at_default_is_now() {
let db = db();
db.execute(r#"(transact [[:alice :person/name "Alice"]])"#)
.unwrap();
let r = db
.execute(
r#"
(query [:find ?vat
:where [:alice :person/name _]
[:alice :db/valid-at ?vat]])
"#,
)
.unwrap();
let rows = results(&r);
assert_eq!(rows.len(), 1);
match rows[0][0] {
Value::Integer(n) => assert!(n > 0, "valid-at default should be a positive timestamp"),
_ => panic!("expected Integer for :db/valid-at default"),
}
}
#[test]
fn valid_at_any_valid_time_is_null() {
let db = db();
db.execute(r#"(transact [[:alice :person/name "Alice"]])"#)
.unwrap();
let r = db
.execute(
r#"
(query [:find ?vat
:any-valid-time
:where [:alice :person/name _]
[:alice :db/valid-at ?vat]])
"#,
)
.unwrap();
let rows = results(&r);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0], Value::Null);
}
#[test]
fn parse_error_pseudo_attr_in_entity_position() {
let db = db();
let result = db.execute(
r#"
(query [:find ?v
:any-valid-time
:where [:db/valid-from :person/name ?v]])
"#,
);
assert!(
result.is_err(),
"pseudo-attribute in entity position must be a parse error"
);
}
#[test]
fn parse_error_pseudo_attr_in_value_position() {
let db = db();
let result = db.execute(
r#"
(query [:find ?e
:any-valid-time
:where [?e :person/name :db/valid-from]])
"#,
);
assert!(
result.is_err(),
"pseudo-attribute in value position must be a parse error"
);
}
#[test]
fn runtime_error_valid_from_without_any_valid_time() {
let db = db();
db.execute(r#"(transact [[:alice :person/name "Alice"]])"#)
.unwrap();
let result = db.execute(
r#"
(query [:find ?vf
:where [:alice :person/name _]
[:alice :db/valid-from ?vf]])
"#,
);
assert!(result.is_err(), ":db/valid-from requires :any-valid-time");
}
#[test]
fn runtime_error_valid_to_without_any_valid_time() {
let db = db();
db.execute(r#"(transact [[:alice :person/name "Alice"]])"#)
.unwrap();
let result = db.execute(
r#"
(query [:find ?vt
:where [:alice :person/name _]
[:alice :db/valid-to ?vt]])
"#,
);
assert!(result.is_err(), ":db/valid-to requires :any-valid-time");
}
#[test]
fn runtime_error_tx_count_without_any_valid_time() {
let db = db();
db.execute(r#"(transact [[:alice :person/name "Alice"]])"#)
.unwrap();
let result = db.execute(
r#"
(query [:find ?tc
:where [:alice :person/name _]
[:alice :db/tx-count ?tc]])
"#,
);
assert!(result.is_err(), ":db/tx-count requires :any-valid-time");
}
#[test]
fn runtime_error_tx_id_without_any_valid_time() {
let db = db();
db.execute(r#"(transact [[:alice :person/name "Alice"]])"#)
.unwrap();
let result = db.execute(
r#"
(query [:find ?ti
:where [:alice :person/name _]
[:alice :db/tx-id ?ti]])
"#,
);
assert!(result.is_err(), ":db/tx-id requires :any-valid-time");
}
#[test]
fn valid_at_succeeds_without_any_valid_time() {
let db = db();
db.execute(r#"(transact [[:alice :person/name "Alice"]])"#)
.unwrap();
let result = db.execute(
r#"
(query [:find ?vat
:where [:alice :person/name _]
[:alice :db/valid-at ?vat]])
"#,
);
assert!(
result.is_ok(),
":db/valid-at must not require :any-valid-time"
);
}
#[test]
fn runtime_error_rules_per_fact_pseudo_without_any_valid_time() {
let db = db();
db.execute(r#"(transact [[:alice :person/name "Alice"] [:alice :parent/of :bob]])"#)
.unwrap();
db.execute(r#"(rule [(ancestor ?x ?y) [?x :parent/of ?y]])"#)
.unwrap();
let result = db.execute(
r#"
(query [:find ?y
:where [(ancestor :alice ?y)]
[:alice :db/tx-count ?tc]])
"#,
);
assert!(
result.is_err(),
":db/tx-count in rules query requires :any-valid-time"
);
}
#[test]
fn or_clause_no_valid_at_executes() {
let db = db();
db.execute(r#"(transact [[:alice :person/name "Alice"]])"#)
.unwrap();
db.execute(r#"(transact [[:bob :person/name "Bob"]])"#)
.unwrap();
let r = db
.execute(
r#"
(query [:find ?n
:where [?e :person/name ?n]
(or [?e :person/name "Alice"] [?e :person/name "Bob"])])
"#,
)
.unwrap();
let rows = results(&r);
assert_eq!(rows.len(), 2, "both Alice and Bob matched via or-clause");
}
#[test]
fn pseudo_attr_as_sole_pattern() {
let db = db();
db.execute(r#"(transact {:valid-from "2022-01-01"} [[:alice :person/name "Alice"]])"#)
.unwrap();
let r = db
.execute(
r#"
(query [:find ?vf
:any-valid-time
:where [:alice :db/valid-from ?vf]])
"#,
)
.unwrap();
let rows = results(&r);
assert_eq!(rows.len(), 1, "one fact for alice");
assert_eq!(
rows[0][0],
Value::Integer(1640995200000),
"valid-from = 2022-01-01"
);
}
#[test]
fn pseudo_attr_constant_filter_no_match() {
let db = db();
db.execute(r#"(transact {:valid-from "2022-01-01"} [[:alice :person/name "Alice"]])"#)
.unwrap();
let r = db
.execute(
r#"
(query [:find ?n
:any-valid-time
:where [:alice :person/name ?n]
[:alice :db/valid-from 9999999999999]])
"#,
)
.unwrap();
let rows = results(&r);
assert_eq!(rows.len(), 0, "no facts with valid-from = 9999999999999");
}
#[test]
fn not_join_body_with_pseudo_attr() {
let db = db();
db.execute(r#"(transact {:valid-from "2020-01-01"} [[:alice :person/name "Alice"]])"#)
.unwrap();
db.execute(r#"(transact {:valid-from "2022-01-01"} [[:bob :person/name "Bob"]])"#)
.unwrap();
let r = db
.execute(
r#"
(query [:find ?e
:any-valid-time
:where [?e :person/name _]
(not-join [?e]
[?e :db/valid-from 1577836800000])])
"#,
)
.unwrap();
let rows = results(&r);
assert_eq!(
rows.len(),
1,
"alice excluded by not-join on valid-from; only bob survives"
);
}
#[test]
fn parse_error_wrong_length_where_pattern() {
let db = db();
let result = db.execute(
r#"
(query [:find ?e :where [?e :person/name]])
"#,
);
assert!(
result.is_err(),
"2-element where pattern must be a parse error"
);
}