# Scripting: mino Mentat embeds [mino](https://codeberg.org/gregburd/mentat) (`crates/mino`), a pure-Rust Clojure-dialect interpreter, and exposes a `mentat.store/*` primitive surface for scripting transactions and queries in a single evaluated program. Upstream mino is archived; this project maintains the interpreter from here on. The same surface is available on both backends: - **Embedded** — `mentat::ScriptInterpreter` (in the `mentat` crate) runs mino scripts against SQLite-backed `Store`s. - **PostgreSQL** — the SQL function `edn_eval(TEXT)` runs a script against the current database. It is behind the optional `script` cargo feature (off by default). `mentat_eval(TEXT)` is its deprecated pre-1.9.0 name. `(mentat.store/open)` and `(mentat.store/open "default")` open the default store; `(mentat.store/open "acme")` opens the named store `acme` (one made with `create_store`), and every read and write through that conn goes to `acme`. An unknown name is `:db.error/store-not-found`. ## The Datomic-style model A script talks to the store through a small set of primitives that follow Datomic's split between a mutable connection and immutable database values: - **conn handle** — an opaque integer from `mentat.store/open`. Mutating and lifecycle operations (`transact`, `close`) take a conn. - **db value** — an immutable snapshot, represented as a mino map tagged `:mentat.store/db` carrying its basis tx and any `as-of`/`since` bounds. `mentat.store/db` turns a conn into a db value at the current basis; `as-of`/`since`/`with` return *new* db values. Reads take a db value (a bare conn integer is accepted and treated as the current db). Entity ids may be integers, ident keywords, or `[:attr val]` lookup refs. ### Primitives | Primitive | Kind | Purpose | |---|---|---| | `mentat.store/open` | conn | open/get a connection handle | | `mentat.store/close` | conn | close a connection (embedded) | | `mentat.store/db` | read | db value at the current basis | | `mentat.store/as-of` | read | db value fixed at a past tx | | `mentat.store/since` | read | db value floored at a tx | | `mentat.store/transact` | write | commit EDN tx data, returns the tx report: `{:mentat.store/tx-id T :mentat.store/tempids {...} :mentat.store/datoms-asserted N :mentat.store/datoms-retracted M :mentat.store/db-after db}` (the counts leave out `:db/txInstant`; both 0 means nothing changed; nil means the store didn't count) | | `mentat.store/with` | write | speculative tx (savepoint, rolled back) | | `mentat.store/q` | read | run a Datalog query against a db value | | `mentat.store/pull` | read | pull a pattern for entity ids | | `mentat.store/entity` | read | attribute map for an entity | | `mentat.store/read` | read | read one attribute value | | `mentat.store/datoms` | read | raw datoms for a db value | | `mentat.store/entities` | read | entities matching a pattern | The embedded layer also registers `mentat.store/q-once`; the PostgreSQL layer registers `mentat.store/q-once` as an alias of `q`. ### Example ```clojure (let [conn (mentat.store/open)] (mentat.store/transact conn [{:db/ident :person/name :db/valueType :db.type/string :db/cardinality :db.cardinality/one}]) (mentat.store/transact conn [{:person/name "Bob"}]) (mentat.store/q (mentat.store/db conn) '[:find ?name :where [?e :person/name ?name]])) ``` From SQL: ```sql SELECT edn_eval($$ (let [conn (mentat.store/open)] (mentat.store/transact conn [{:person/name "Bob"}]) (mentat.store/q (mentat.store/db conn) '[:find ?name :where [?e :person/name ?name]])) $$); ``` `edn_eval` returns the result as EDN text (`pr-str`). A mino exception surfaces as a clean PostgreSQL `ERROR` carrying the mino message. ### Temporal reads across backends The PostgreSQL query engine accepts `as-of`/`since` temporal inputs, so arbitrary Datalog `mentat.store/q` runs faithfully against a historical basis: an as-of/since db value forwards its bound into the query inputs. The embedded (SQLite) and DuckDB backends run the same temporal Datalog, so `q`, `pull`, `entity`, `read`, `entities` and `datoms` honor `as-of`/`since` on every backend. `pull` on an as-of T db returns the values as of T, including inside nested map specs and in a find-pull (`[:find (pull ?e [...]) ...]`). Before 1.18.0 the embedded backend refused `pull` and `entities` on a historical db, and pg and DuckDB returned current values. ## Security — `edn_eval` `edn_eval` is **callable by every role by design**. The extension issues no `REVOKE`, so PostgreSQL's default grants `EXECUTE` to `PUBLIC`. That makes the sandbox the entire defense, so it is built to hold against a hostile caller, not just a careless one. - **Sandboxed — no host access.** The interpreter is built with `mino_rs::Interpreter::sandboxed()`: the language, regex, bignum, atoms, and the in-memory `mentat.store/*` surface are present, but every host-filesystem primitive (`slurp`, `spit`, `rm-rf`, `mkdir-p`, `file-exists?`) and the file-backed store are absent — unbound, not merely refused. A script cannot reach the server's filesystem. - **Resource-limited.** Three GUCs bound every call. All are `PGC_SUSET`: a superuser (or `ALTER SYSTEM`) sets them, and an ordinary role cannot raise them for its own session. | GUC | Default | |---|---| | `mentat.script_max_steps` | 10,000,000 | | `mentat.script_max_heap_bytes` | 64 MiB | | `mentat.script_max_depth` | 2,000 | These guard, respectively, against a runaway loop (`(loop [] (recur))`), a single step that allocates unboundedly (`(range 1e11)`), and deep non-tail recursion that would otherwise overflow the stack and crash the backend. Exceeding any of them throws an `:eval/limit` error. `statement_timeout` still applies on top, via an interrupt check hook that also wires in `pg_cancel_backend()` and `stack_is_too_deep()`. - **No privilege gain.** A script runs through SPI as the calling role, so it reaches only the stores that role can already query with `edn_q`/`edn_t` or `q`/`t`: opening a named store the role has no `USAGE` on (schema `mentat_`) fails with a permission error. It must **not** be made `SECURITY DEFINER` — that would turn it into a privilege escalation. - **Transaction functions reach one store.** A `:db/fn` runs its author's code with the privileges of the role that invokes it, so on PostgreSQL it reads only the store it runs on (the store being transacted): `(mentat.store/open)` and `(mentat.store/open "")` open it, and any other name is `:db.error/txfn-store-scope`. It can't transact or call `with` (`:db.error/txfn-transact`): it returns tx-data, which joins the invoking transaction. An `edn_eval` script runs the caller's own code, so it may open every store the caller can reach. The embedded `ScriptInterpreter` uses the same sandboxed interpreter with step/heap/depth limits set by the host.