use minigraf::{Minigraf, QueryResult, Value};
fn exec(db: &Minigraf, input: &str) -> QueryResult {
db.execute(input)
.unwrap_or_else(|e| panic!("execution error for {:?}: {}", input, e))
}
fn result_rows(result: QueryResult) -> Vec<Vec<Value>> {
match result {
QueryResult::QueryResults { results, .. } => results,
other => panic!("expected QueryResults, got {:?}", other),
}
}
#[test]
fn test_tx_time_travel_via_counter() {
let db = Minigraf::in_memory().unwrap();
exec(&db, r#"(transact [[:alice :person/name "Alice"]])"#);
exec(&db, r#"(transact [[:alice :person/age "30"]])"#);
let result = exec(
&db,
r#"(query [:find ?attr :as-of 1 :valid-at :any-valid-time :where [:alice ?attr ?v]])"#,
);
let rows = result_rows(result);
assert_eq!(rows.len(), 1, "as-of tx 1 should see only the name fact");
match &rows[0][0] {
Value::Keyword(k) => assert_eq!(k, ":person/name"),
other => panic!("expected keyword :person/name, got {:?}", other),
}
}
#[test]
fn test_tx_time_travel_as_of_all() {
let db = Minigraf::in_memory().unwrap();
exec(&db, r#"(transact [[:alice :person/name "Alice"]])"#);
exec(&db, r#"(transact [[:alice :person/age "30"]])"#);
let result = exec(
&db,
r#"(query [:find ?attr :as-of 2 :valid-at :any-valid-time :where [:alice ?attr ?v]])"#,
);
let rows = result_rows(result);
assert_eq!(rows.len(), 2, "as-of tx 2 should see both facts");
}
#[test]
fn test_valid_at_inside_range() {
let db = Minigraf::in_memory().unwrap();
exec(
&db,
r#"(transact {:valid-from "2023-01-01" :valid-to "2023-06-30"} [[:alice :employment/status :active]])"#,
);
let result = exec(
&db,
r#"(query [:find ?s :valid-at "2023-03-01" :where [:alice :employment/status ?s]])"#,
);
let rows = result_rows(result);
assert_eq!(
rows.len(),
1,
"2023-03-01 is inside the valid range, should return 1 result"
);
match &rows[0][0] {
Value::Keyword(k) => assert_eq!(k, ":active"),
other => panic!("expected :active, got {:?}", other),
}
}
#[test]
fn test_valid_at_outside_range() {
let db = Minigraf::in_memory().unwrap();
exec(
&db,
r#"(transact {:valid-from "2023-01-01" :valid-to "2023-06-30"} [[:alice :employment/status :active]])"#,
);
let result = exec(
&db,
r#"(query [:find ?s :valid-at "2024-01-01" :where [:alice :employment/status ?s]])"#,
);
let rows = result_rows(result);
assert_eq!(
rows.len(),
0,
"2024-01-01 is outside the valid range, should return 0 results"
);
}
#[test]
fn test_no_valid_at_returns_only_current() {
let db = Minigraf::in_memory().unwrap();
exec(
&db,
r#"(transact {:valid-from "2020-01-01" :valid-to "2020-12-31"} [[:alice :employment/org :old-company]])"#,
);
exec(&db, r#"(transact [[:alice :person/name "Alice"]])"#);
let result = exec(&db, r#"(query [:find ?attr :where [:alice ?attr ?v]])"#);
let rows = result_rows(result);
assert_eq!(
rows.len(),
1,
"default query should return only currently valid facts"
);
match &rows[0][0] {
Value::Keyword(k) => assert_eq!(k, ":person/name"),
other => panic!("expected :person/name, got {:?}", other),
}
}
#[test]
fn test_valid_at_any_valid_time_returns_all() {
let db = Minigraf::in_memory().unwrap();
exec(
&db,
r#"(transact {:valid-from "2020-01-01" :valid-to "2020-12-31"} [[:alice :employment/org :old-company]])"#,
);
exec(&db, r#"(transact [[:alice :person/name "Alice"]])"#);
let result = exec(
&db,
r#"(query [:find ?attr :valid-at :any-valid-time :where [:alice ?attr ?v]])"#,
);
let rows = result_rows(result);
assert_eq!(
rows.len(),
2,
":any-valid-time should return both expired and current facts"
);
}
#[test]
fn test_bitemporal_combined_query() {
let db = Minigraf::in_memory().unwrap();
exec(
&db,
r#"(transact {:valid-from "2023-01-01" :valid-to "2023-06-30"} [[:alice :employment/status :active]])"#,
);
exec(
&db,
r#"(transact {:valid-from "2023-01-01" :valid-to "2023-06-30"} [[:alice :employment/status :inactive]])"#,
);
let result = exec(
&db,
r#"(query [:find ?s :as-of 1 :valid-at "2023-03-01" :where [:alice :employment/status ?s]])"#,
);
let rows = result_rows(result);
assert_eq!(
rows.len(),
1,
"as-of tx 1 should see only the original :active fact"
);
match &rows[0][0] {
Value::Keyword(k) => assert_eq!(k, ":active", "expected :active at tx_count=1"),
other => panic!("expected keyword, got {:?}", other),
}
}
#[test]
fn test_valid_at_boundary_exclusive() {
let db = Minigraf::in_memory().unwrap();
exec(
&db,
r#"(transact {:valid-from "2023-01-01" :valid-to "2023-06-30"} [[:alice :employment/status :active]])"#,
);
let result = exec(
&db,
r#"(query [:find ?s :valid-at "2023-06-30" :where [:alice :employment/status ?s]])"#,
);
let rows = result_rows(result);
assert_eq!(
rows.len(),
0,
"valid_to is exclusive: querying at exactly valid_to should return no results"
);
let result2 = exec(
&db,
r#"(query [:find ?s :valid-at "2023-06-29" :where [:alice :employment/status ?s]])"#,
);
let rows2 = result_rows(result2);
assert_eq!(
rows2.len(),
1,
"one day before valid_to should still be in range"
);
}
#[test]
fn test_bitemporal_multi_entity() {
let db = Minigraf::in_memory().unwrap();
exec(
&db,
r#"(transact [[:alice-kw :person/name "Alice"] [:bob-kw :person/name "Bob"]])"#,
);
exec(
&db,
r#"(transact {:valid-from "2023-01-01" :valid-to "2023-06-30"} [[:alice-kw :employment/org :acme]])"#,
);
exec(
&db,
r#"(transact {:valid-from "2023-07-01" :valid-to "2023-12-31"} [[:bob-kw :employment/org :beta]])"#,
);
let result = exec(
&db,
r#"(query [:find ?who :valid-at "2023-03-01" :where [?who :employment/org ?org]])"#,
);
let rows = result_rows(result);
assert_eq!(
rows.len(),
1,
"only alice-kw should be employed at 2023-03-01"
);
let result2 = exec(
&db,
r#"(query [:find ?who :valid-at "2023-09-01" :where [?who :employment/org ?org]])"#,
);
let rows2 = result_rows(result2);
assert_eq!(
rows2.len(),
1,
"only bob-kw should be employed at 2023-09-01"
);
}
#[test]
fn test_as_of_counter_time_travel() {
let db = Minigraf::in_memory().unwrap();
exec(&db, r#"(transact [[:alice :person/name "Alice"]])"#);
exec(&db, r#"(transact [[:alice :person/age "30"]])"#);
exec(&db, r#"(transact [[:alice :person/city "NYC"]])"#);
let result1 = exec(
&db,
r#"(query [:find ?attr :as-of 1 :valid-at :any-valid-time :where [:alice ?attr ?v]])"#,
);
assert_eq!(result_rows(result1).len(), 1, "as-of 1: only name");
let result2 = exec(
&db,
r#"(query [:find ?attr :as-of 2 :valid-at :any-valid-time :where [:alice ?attr ?v]])"#,
);
assert_eq!(result_rows(result2).len(), 2, "as-of 2: name + age");
let result3 = exec(
&db,
r#"(query [:find ?attr :as-of 3 :valid-at :any-valid-time :where [:alice ?attr ?v]])"#,
);
assert_eq!(result_rows(result3).len(), 3, "as-of 3: name + age + city");
}