use mino_rs::Interpreter;
pub fn seed(it: &mut Interpreter) {
it.eval("(def c (mentat.store/open))").expect("open");
it.eval_to_string(
"(mentat.store/transact c \
[{:db/ident :person/name :db/valueType :db.type/string \
:db/cardinality :db.cardinality/one}])",
)
.expect("schema transact");
it.eval_to_string("(mentat.store/transact c [{:person/name \"Alice\"}])")
.expect("data transact");
}
pub fn eid_of(it: &mut Interpreter, name: &str) -> String {
it.eval_to_string(&format!(
"(mentat.store/q (mentat.store/db c) '[:find ?e . :where [?e :person/name \"{name}\"]])"
))
.expect("eid query")
}
pub const TX0: i64 = 0x1000_0000;
pub fn db_is_an_immutable_value_not_the_conn(it: &mut Interpreter) {
seed(it);
assert_eq!(it.eval_to_string("c").unwrap(), "1");
let db = it.eval_to_string("(mentat.store/db c)").unwrap();
assert!(db.contains(":mentat.store/db true"), "{db}");
assert!(db.contains(":mentat.store/basis-tx"), "{db}");
assert!(db.contains(":mentat.store/conn 1"), "{db}");
assert_ne!(db, "1");
let basis = it
.eval_to_string("(:mentat.store/basis-tx (mentat.store/db c))")
.unwrap();
assert!(basis.parse::<i64>().unwrap() > TX0, "basis {basis}");
}
pub fn q_takes_a_db_value(it: &mut Interpreter) {
seed(it);
assert_eq!(
it.eval_to_string(
"(mentat.store/q (mentat.store/db c) '[:find ?n :where [?e :person/name ?n]])"
)
.unwrap(),
"#{[\"Alice\"]}"
);
assert_eq!(
it.eval_to_string("(mentat.store/q c '[:find ?n :where [?e :person/name ?n]])")
.unwrap(),
"#{[\"Alice\"]}"
);
}
pub fn pull_returns_a_map(it: &mut Interpreter) {
seed(it);
let eid = eid_of(it, "Alice");
let pulled = it
.eval_to_string(&format!(
"(mentat.store/pull (mentat.store/db c) {eid} [:person/name])"
))
.unwrap();
assert!(pulled.starts_with('{'), "pull -> map: {pulled}");
assert!(pulled.contains(":person/name \"Alice\""), "pull: {pulled}");
}
pub fn entity_returns_an_entity_map(it: &mut Interpreter) {
seed(it);
let eid = eid_of(it, "Alice");
let ent = it
.eval_to_string(&format!("(mentat.store/entity (mentat.store/db c) {eid})"))
.unwrap();
assert!(ent.contains(&format!(":db/id {eid}")), "{ent}");
assert!(ent.contains(":person/name \"Alice\""), "{ent}");
}
pub fn read_returns_the_scalar(it: &mut Interpreter) {
seed(it);
let eid = eid_of(it, "Alice");
assert_eq!(
it.eval_to_string(&format!(
"(mentat.store/read (mentat.store/db c) {eid} :person/name)"
))
.unwrap(),
"\"Alice\""
);
}
pub fn datoms_returns_tuples(it: &mut Interpreter) {
seed(it);
let ds = it
.eval_to_string("(mentat.store/datoms (mentat.store/db c))")
.unwrap();
assert!(ds.starts_with('['), "datoms: {ds}");
assert!(
ds.contains("\"Alice\""),
"datoms should include Alice: {ds}"
);
}
pub fn with_is_speculative_and_does_not_commit(it: &mut Interpreter) {
seed(it);
let before = it
.eval_to_string("(mentat.store/q c '[:find ?n :where [?e :person/name ?n]])")
.unwrap();
assert_eq!(before, "#{[\"Alice\"]}");
let with_result = it
.eval_to_string("(mentat.store/with (mentat.store/db c) [{:person/name \"Bob\"}])")
.unwrap();
assert!(
with_result.contains(":mentat.store/db-after"),
"{with_result}"
);
assert!(
with_result.contains(":mentat.store/tx-report"),
"{with_result}"
);
let basis_now: i64 = it
.eval_to_string("(:mentat.store/basis-tx (mentat.store/db c))")
.unwrap()
.parse()
.unwrap();
let db_after_basis: i64 = it
.eval_to_string(
"(:mentat.store/basis-tx (:mentat.store/db-after \
(mentat.store/with (mentat.store/db c) [{:person/name \"Bob\"}])))",
)
.unwrap()
.parse()
.unwrap();
assert!(db_after_basis > basis_now, "{db_after_basis} > {basis_now}");
let after = it
.eval_to_string(
"(mentat.store/q (mentat.store/db c) '[:find ?n :where [?e :person/name ?n]])",
)
.unwrap();
assert_eq!(after, "#{[\"Alice\"]}", "with must not commit");
}
pub fn as_of_and_since_reflect_the_basis(it: &mut Interpreter) {
seed(it);
let basis_alice: i64 = it
.eval_to_string("(:mentat.store/basis-tx (mentat.store/db c))")
.unwrap()
.parse()
.unwrap();
it.eval_to_string("(mentat.store/transact c [{:person/name \"Bob\"}])")
.unwrap();
let basis_bob: i64 = it
.eval_to_string("(:mentat.store/basis-tx (mentat.store/db c))")
.unwrap()
.parse()
.unwrap();
assert!(basis_bob > basis_alice);
let as_of_alice = it
.eval_to_string(&format!(
"(mentat.store/datoms (mentat.store/as-of (mentat.store/db c) {basis_alice}))"
))
.unwrap();
assert!(as_of_alice.contains("Alice"), "{as_of_alice}");
assert!(!as_of_alice.contains("Bob"), "{as_of_alice}");
let as_of_bob = it
.eval_to_string(&format!(
"(mentat.store/datoms (mentat.store/as-of (mentat.store/db c) {basis_bob}))"
))
.unwrap();
assert!(
as_of_bob.contains("Alice") && as_of_bob.contains("Bob"),
"{as_of_bob}"
);
let since_alice = it
.eval_to_string(&format!(
"(mentat.store/datoms (mentat.store/since (mentat.store/db c) {basis_alice}))"
))
.unwrap();
assert!(since_alice.contains("Bob"), "{since_alice}");
assert!(!since_alice.contains("Alice"), "{since_alice}");
let as_of_db = it
.eval_to_string(&format!(
"(mentat.store/as-of (mentat.store/db c) {basis_alice})"
))
.unwrap();
assert!(
as_of_db.contains(&format!(":mentat.store/as-of {basis_alice}")),
"{as_of_db}"
);
let since_db = it
.eval_to_string(&format!(
"(mentat.store/since (mentat.store/db c) {basis_alice})"
))
.unwrap();
assert!(
since_db.contains(&format!(":mentat.store/since {basis_alice}")),
"{since_db}"
);
}
pub fn q_runs_against_as_of_and_since(it: &mut Interpreter) {
seed(it);
let basis_alice: i64 = it
.eval_to_string("(:mentat.store/basis-tx (mentat.store/db c))")
.unwrap()
.parse()
.unwrap();
it.eval_to_string("(mentat.store/transact c [{:person/name \"Bob\"}])")
.unwrap();
let names = "'[:find ?n :where [?e :person/name ?n]]";
let now = it
.eval_to_string(&format!("(mentat.store/q (mentat.store/db c) {names})"))
.unwrap();
assert!(now.contains("Alice") && now.contains("Bob"), "{now}");
let then = it
.eval_to_string(&format!(
"(mentat.store/q (mentat.store/as-of (mentat.store/db c) {basis_alice}) {names})"
))
.unwrap();
assert_eq!(then, "#{[\"Alice\"]}", "as-of must hide later facts");
let with_in = it
.eval_to_string(&format!(
"(mentat.store/q (mentat.store/as-of (mentat.store/db c) {basis_alice}) \
'[:find ?e :in $ ?n :where [?e :person/name ?n]] \"Bob\")"
))
.unwrap();
assert_eq!(
with_in, "#{}",
"Bob did not exist as of Alice's basis: {with_in}"
);
}
pub fn tx_report_has_the_datomic_shape(it: &mut Interpreter) {
seed(it);
let report = it
.eval_to_string("(mentat.store/transact c [{:person/name \"Carol\"}])")
.unwrap();
assert!(report.contains(":mentat.store/tx-id"), "{report}");
assert!(report.contains(":mentat.store/tempids"), "{report}");
assert!(report.contains(":mentat.store/db-after"), "{report}");
}
pub fn inst_and_uuid_builders_round_trip_through_the_reader() {
use mino_rs::printer::print_str;
use mino_rs::reader::read_one;
let uuid = crate::values::uuid_value("12345678-1234-5678-1234-567812345678");
assert_eq!(
print_str(&uuid),
"#uuid \"12345678-1234-5678-1234-567812345678\""
);
let inst = crate::values::inst_value("2017-01-01T00:00:00Z");
assert_eq!(
print_str(&inst),
"(clojure.instant/read-instant-date \"2017-01-01T00:00:00Z\")"
);
for v in [uuid, inst] {
let (v2, _) = read_one(&print_str(&v)).unwrap();
assert_eq!(print_str(&v), print_str(&v2));
}
}
pub fn q_takes_in_inputs(it: &mut Interpreter) {
seed(it);
it.eval_to_string("(mentat.store/transact c [{:person/name \"Bob\"}])")
.unwrap();
assert_eq!(
it.eval_to_string(
"(mentat.store/q (mentat.store/db c) \
'[:find ?n :in $ ?n :where [?e :person/name ?n]] \"Bob\")"
)
.unwrap(),
"#{[\"Bob\"]}"
);
let both = it
.eval_to_string(
"(mentat.store/q (mentat.store/db c) \
'[:find ?n :in $ [?n ...] :where [?e :person/name ?n]] [\"Alice\" \"Bob\" \"Zed\"])",
)
.unwrap();
assert!(
both.contains("\"Alice\"") && both.contains("\"Bob\"") && !both.contains("Zed"),
"{both}"
);
assert!(it
.eval(
"(mentat.store/q (mentat.store/db c) '[:find ?n :in $ ?n :where [?e :person/name ?n]])"
)
.is_err());
}
fn seed_people(it: &mut Interpreter) {
it.eval("(def c (mentat.store/open))").expect("open");
it.eval_to_string(
"(mentat.store/transact c \
[{:db/ident :person/name :db/valueType :db.type/string \
:db/cardinality :db.cardinality/one :db/unique :db.unique/identity :db/index true} \
{:db/ident :person/age :db/valueType :db.type/long :db/cardinality :db.cardinality/one} \
{:db/ident :person/friend :db/valueType :db.type/ref :db/cardinality :db.cardinality/many}])",
)
.expect("schema");
it.eval_to_string(
"(mentat.store/transact c \
[{:db/id \"a\" :person/name \"Alice\" :person/age 30 :person/friend \"b\"} \
{:db/id \"b\" :person/name \"Bob\" :person/age 40}])",
)
.expect("people");
}
pub fn q_mixed_inputs(it: &mut Interpreter) {
seed_people(it);
let bob = it
.eval_to_string(
"(mentat.store/q (mentat.store/db c) '[:find ?e . :where [?e :person/name \"Bob\"]])",
)
.unwrap();
assert_eq!(
it.eval_to_string(&format!(
"(mentat.store/q (mentat.store/db c) \
'[:find ?n . :in $ ?f [?a ...] :where [?e :person/friend ?f] [?e :person/age ?a] [?e :person/name ?n]] \
{bob} [30 31])"
))
.unwrap(),
"\"Alice\""
);
let rel = it
.eval_to_string(
"(mentat.store/q (mentat.store/db c) \
'[:find ?n :in $ [[?n ?a]] :where [?e :person/name ?n] [?e :person/age ?a]] \
[[\"Alice\" 30] [\"Bob\" 41]])",
)
.unwrap();
assert_eq!(rel, "#{[\"Alice\"]}");
}
fn person(it: &mut Interpreter, name: &str) -> String {
it.eval_to_string(&format!(
"(mentat.store/q (mentat.store/db c) '[:find ?e . :where [?e :person/name \"{name}\"]])"
))
.unwrap()
}
pub fn history_patterns_see_added(it: &mut Interpreter) {
seed_people(it);
let alice = person(it, "Alice");
it.eval_to_string(&format!(
"(mentat.store/transact c [[:db/add {alice} :person/age 31]])"
))
.unwrap();
let h = it
.eval_to_string(
"(mentat.store/q (mentat.store/db c) \
'[:find ?v ?added :where [?e :person/age ?v ?tx ?added] [?e :person/name \"Alice\"]])",
)
.unwrap();
for want in ["[30 true]", "[30 false]", "[31 true]"] {
assert!(h.contains(want), "history should contain {want}: {h}");
}
}
pub fn cas_and_retract_entity(it: &mut Interpreter) {
seed_people(it);
let (alice, bob) = (person(it, "Alice"), person(it, "Bob"));
let age = "(mentat.store/q (mentat.store/db c) \
'[:find ?a . :where [?e :person/name \"Alice\"] [?e :person/age ?a]])";
it.eval_to_string(&format!(
"(mentat.store/transact c [[:db/cas {alice} :person/age 30 31]])"
))
.expect("cas from the right value");
assert_eq!(it.eval_to_string(age).unwrap(), "31");
assert!(
it.eval(&format!(
"(mentat.store/transact c [[:db/cas {alice} :person/age 30 32]])"
))
.is_err(),
"cas from a stale value must fail"
);
assert_eq!(it.eval_to_string(age).unwrap(), "31");
it.eval_to_string(
"(mentat.store/transact c [[:db/cas [:person/name \"Alice\"] :person/age 31 32]])",
)
.expect("cas via a lookup ref");
assert_eq!(it.eval_to_string(age).unwrap(), "32");
let friends = "(mentat.store/q (mentat.store/db c) \
'[:find ?f . :where [?e :person/name \"Alice\"] [?e :person/friend ?f]])";
assert_eq!(it.eval_to_string(friends).unwrap(), bob);
it.eval_to_string("(mentat.store/transact c [[:db/retractEntity [:person/name \"Bob\"]]])")
.expect("retractEntity via a lookup ref");
assert_eq!(person(it, "Bob"), "nil");
assert_eq!(
it.eval_to_string(&format!(
"(mentat.store/q (mentat.store/db c) '[:find ?a . :where [{bob} :person/age ?a]])"
))
.unwrap(),
"nil"
);
assert_eq!(
it.eval_to_string(friends).unwrap(),
"nil",
"inbound ref retracted"
);
assert_eq!(person(it, "Alice"), alice);
}
pub fn transaction_functions(it: &mut Interpreter) {
seed_people(it);
let age = |it: &mut Interpreter, who: &str| {
it.eval_to_string(&format!(
"(mentat.store/q (mentat.store/db c) \
'[:find ?a . :where [?e :person/name \"{who}\"] [?e :person/age ?a]])"
))
.unwrap()
};
it.eval_to_string(
"(mentat.store/transact c \
[{:db/ident :person/birthday \
:db/fn \"(fn [db who] \
(let [a (ffirst (mentat.store/q db \
'[:find ?a :in $ ?n :where [?e :person/name ?n] [?e :person/age ?a]] \
who))] \
[[:db/cas [:person/name who] :person/age a (inc a)]]))\"}])",
)
.expect("install :person/birthday");
it.eval_to_string("(mentat.store/transact c [[:person/birthday \"Alice\"]])")
.expect("invoke");
assert_eq!(age(it, "Alice"), "31");
it.eval_to_string(
"(mentat.store/transact c \
[{:db/ident :person/two-birthdays \
:db/fn \"(fn [db who] [[:person/birthday who]])\"}])",
)
.expect("install nested fn");
it.eval_to_string(
"(mentat.store/transact c [[:person/two-birthdays \"Bob\"] {:person/name \"Carol\" :person/age 7}])",
)
.expect("nested invoke");
assert_eq!(age(it, "Bob"), "41");
assert_eq!(age(it, "Carol"), "7");
it.eval_to_string(
"(mentat.store/transact c [{:db/ident :person/boom :db/fn \"(fn [db] (throw (ex-info \\\"nope\\\" {})))\"}])",
)
.expect("install :person/boom");
let err = it
.eval_to_string(
"(mentat.store/transact c [[:person/boom] {:person/name \"Dan\" :person/age 1}])",
)
.expect_err("a throwing tx-fn aborts the tx");
assert!(
err.contains("txfn-error") && err.contains(":person/boom"),
"{err}"
);
assert_eq!(age(it, "Dan"), "nil", "nothing from the aborted tx");
it.eval_to_string(
"(mentat.store/transact c [{:db/ident :person/forever :db/fn \"(fn [db] [[:person/forever]])\"}])",
)
.expect("install :person/forever");
let err = it
.eval_to_string("(mentat.store/transact c [[:person/forever]])")
.expect_err("unbounded recursion");
assert!(err.contains("txfn-depth"), "{err}");
it.eval_to_string(
"(mentat.store/transact c [{:db/ident :person/escape :db/fn \"(fn [db] (slurp \\\"/etc/passwd\\\") [])\"}])",
)
.expect("install :person/escape");
let err = it
.eval_to_string("(mentat.store/transact c [[:person/escape]])")
.expect_err("slurp must be unavailable");
assert!(err.contains("txfn-error"), "{err}");
}