mentat_script/lib.rs
1//! Backend-independent mino `mentat.store/*` scripting glue (plan § 1.19).
2//!
3//! Both Mentat backends — the embedded SQLite [`Store`] and the pg_mentat
4//! engine — expose the same Datomic-a-like `mentat.store/*` language surface
5//! (14 prims). This crate holds everything that surface needs that does **not**
6//! depend on the storage engine:
7//!
8//! * the db-value map shape and its destructuring ([`values::DbRef`],
9//! [`values::db_value`], [`values::destructure_db`]);
10//! * the tx-report projection and the tagged inst/uuid/keyword value builders
11//! ([`values`]);
12//! * argument parsing (conn handle, eid, keyword, tx id);
13//! * [`install`], which registers all 14 `mentat.store/*` prims onto a mino
14//! interpreter, driving each through a [`ScriptBackend`].
15//!
16//! A backend implements [`ScriptBackend`] (storage + its own result
17//! conversion: `Binding→Value` for Mentat, `JSON→Value` for pg_mentat) and
18//! calls [`install`]. The Datomic-model test suite (`tests/model.rs`) runs
19//! against a fake in-memory backend here, and against both real backends in
20//! their own crates — one suite, proven three ways.
21
22pub mod model_tests;
23pub mod prims;
24pub mod values;
25
26pub use prims::install;
27pub use values::{
28 db_value, destructure_db, inst_value, kw_ns, str_val, tx_report_value, DbRef, TxReport,
29};
30
31use mino_rs::Value;
32
33/// The storage seam between the shared `mentat.store/*` surface and a concrete
34/// engine. A backend owns storage and *its own* result conversion (each read
35/// method returns an already-built mino [`Value`]); the shared [`install`]
36/// owns argument parsing, the db-value shape, and result wrapping.
37///
38/// `entity`/`read`/`entities` have default implementations on top of `q`/`pull`
39/// so a minimal backend (the fake, pg) needs only the core methods; the SQLite
40/// backend overrides them because its as-of/since reads reconstruct the basis
41/// from the transaction log rather than running Datalog.
42pub trait ScriptBackend {
43 /// `(open)` / `(open "path")` → a conn handle. In-memory when `path` is
44 /// `None`/empty.
45 fn open(&mut self, path: Option<&str>) -> Result<i64, String>;
46
47 /// `(close conn)`. Drop/forget the store, if the backend owns one.
48 fn close(&mut self, conn: i64) -> Result<(), String>;
49
50 /// The current basis (max committed tx) for a conn.
51 fn basis_tx(&self, conn: i64) -> Result<i64, String>;
52
53 /// Commit `edn` against `conn`; return the report.
54 fn transact(&mut self, conn: i64, edn: &str) -> Result<TxReport, String>;
55
56 /// Speculatively transact `edn` against `db` WITHOUT committing; return the
57 /// resulting basis tx and the report.
58 fn with(&mut self, db: &DbRef, edn: &str) -> Result<(i64, TxReport), String>;
59
60 /// Run Datalog `query_edn` against `db`; return the result already
61 /// converted to a mino [`Value`] in Datomic result shape.
62 fn q(&self, db: &DbRef, query_edn: &str) -> Result<Value, String>;
63
64 /// `(q db query arg1 arg2 ...)`: `q` with the `:in` values after `$`, in
65 /// order, as JSON (the `"inputs"` array of SQL `edn_q`'s options: numbers,
66 /// strings, `":kw"` keywords, booleans, and arrays for collection / tuple
67 /// / relation bindings). Default: only the no-input case.
68 fn q_with_inputs(
69 &self,
70 db: &DbRef,
71 query_edn: &str,
72 inputs: &[serde_json::Value],
73 ) -> Result<Value, String> {
74 if inputs.is_empty() {
75 self.q(db, query_edn)
76 } else {
77 Err("this backend does not take :in arguments".into())
78 }
79 }
80
81 /// Pull `pattern_edn` for a single `eid` against `db` → a mino map.
82 fn pull(&self, db: &DbRef, eid: i64, pattern_edn: &str) -> Result<Value, String>;
83
84 /// `(datoms db)` → a vector of `[e a v …]` tuple vectors for the basis.
85 fn datoms(&self, db: &DbRef) -> Result<Value, String>;
86
87 /// Resolve an eid argument (integer, ident keyword, or `[:attr val]`
88 /// lookup-ref) to a numeric entid. Backend-specific because it consults the
89 /// schema / a lookup query.
90 fn resolve_eid(&self, db: &DbRef, arg: &Value) -> Result<i64, String>;
91
92 /// `(entity db eid)` → `{:db/id eid :attr v …}`. Default: pull `[*]`,
93 /// then splice in `:db/id`. A backend whose historical reads reconstruct
94 /// the basis overrides this.
95 fn entity(&self, db: &DbRef, eid: i64) -> Result<Value, String> {
96 use mino_rs::collections::map::PMap;
97 use mino_rs::{Gc, Value as V};
98 let pulled = self.pull(db, eid, "[*]")?;
99 let mut m = PMap::empty().assoc(values::kw_ns("db", "id"), V::Int(eid));
100 if let V::Map(ref pm) = pulled {
101 for (k, v) in pm.entries() {
102 m = m.assoc(k.clone(), v.clone());
103 }
104 }
105 Ok(V::Map(Gc::new(m)))
106 }
107
108 /// `(read db eid attr)` → the value(s) for that eid/attr: cardinality-one
109 /// collapses to the scalar (nil if absent), many stays a set. Default: a
110 /// collection query over `attr`.
111 fn read(&self, db: &DbRef, eid: i64, attr: &str) -> Result<Value, String> {
112 use mino_rs::collections::map::PSet;
113 use mino_rs::{Gc, Value as V};
114 let query = format!("[:find [?v ...] :where [{eid} {attr} ?v]]");
115 // The collection query yields a vector of the attr's values.
116 match self.q(db, &query)? {
117 V::Vector(ref v) => match v.len() {
118 0 => Ok(V::Nil),
119 1 => Ok(v.nth(0).cloned().unwrap_or(V::Nil)),
120 _ => {
121 let mut set = PSet::empty();
122 for x in v.iter() {
123 set = set.conj(x.clone());
124 }
125 Ok(V::Set(Gc::new(set)))
126 }
127 },
128 V::Nil => Ok(V::Nil),
129 other => Ok(other),
130 }
131 }
132
133 /// `(entities db attr)` → a set of eids having `attr`. Default: a
134 /// collection query.
135 fn entities(&self, db: &DbRef, attr: &str) -> Result<Value, String> {
136 use mino_rs::collections::map::PSet;
137 use mino_rs::{Gc, Value as V};
138 let query = format!("[:find [?e ...] :where [?e {attr} _]]");
139 let mut set = PSet::empty();
140 if let V::Vector(ref v) = self.q(db, &query)? {
141 for e in v.iter() {
142 set = set.conj(e.clone());
143 }
144 }
145 Ok(V::Set(Gc::new(set)))
146 }
147}