Skip to main content

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}