Contents

Changelog

All notable changes to mentat (the embedded SQLite store and the pg_mentat PostgreSQL extension, which share one front-end) are documented in this file.

The format is based on Keep a Changelog, and the project follows Semantic Versioning.

[Unreleased]

[1.10.1] — re-release of 1.10.0

1.10.0’s release build never published: the SQLite and DuckDB extension smoke tests failed on Ubuntu CI runners in a check that counts the session’s open handles on the store file (it assumed bash runs .system commands; Ubuntu’s dash runs them differently). Only the test scripts changed. Everything in 1.10.0 below ships in 1.10.1.

Upgrade

ALTER EXTENSION pg_mentat UPDATE TO '1.10.1'; from 1.9.0, 1.9.1 or 1.10.0. From 1.10.0 it changes nothing; from 1.9.x it is the 1.10.0 upgrade below.

[1.10.0] — automatic indexes, faster count, stable at scale

The fixes for everything the 1.9.0 scale benchmark found (benchmarks/results/scale-2026-09-27T010840Z/), with before/after runs in benchmarks/results/{pg-autoindex,embedded-fixes,ext-cache,duckdb-quack}-*.

Added

  • Automatic index management on both engines. Mentat creates value indexes where queries need them and drops the ones it created once they go unused.
    • PostgreSQL: every current_* table gets an AVET index (store_id, a, v, e). mentat.auto_index = off | schema (default) | adaptive; in adaptive mode, range-filtered history attributes get partial indexes, tracked in mentat.managed_indexes and dropped after mentat.auto_index_idle_window (7 days) without scans. mentat_tune_indexes(dry_run) reports or applies the changes. Tuning never blocks a transaction (lock_timeout, skip and log).
    • Embedded: :db/unique attributes and :db/index refs get a usable value index in the default mode (the schema’s old partial AVET indexes never matched mentat’s SQL). AutoIndex::Adaptive adds per-attribute indexes after repeated value-filtered queries, rejects unselective ones, and drops idle ones. Store::tune_indexes, Store::set_auto_index, MENTAT_AUTO_INDEX. Only indexes in the mentat_managed_indexes registry are ever dropped.
  • edn_q_rows(query, inputs) (PostgreSQL): streams one JSONB array per row, for results too large for edn_q’s single JSONB value.
  • DuckDB as a server: crates/duckdb/server/serve.sh runs mentat inside a long-lived DuckDB Quack server (quack_serve, token auth, localhost by default), so clients without mentat send edn_q/edn_t over quack_query. Measured: each call costs about 2.2 ms over in-process DuckDB (Quack opens three TCP connections per call), so in-process with the store cache is always faster; Quack’s hard-coded listen backlog of 5 causes 1-5 s stalls from about 32 clients. See the DuckDB README.
  • CLI: .q takes the same options JSON as SQL edn_q (inputs, asOf, since), plus .pull, .eval (--features mino), .tune / .tune!, and a batch mode (-e, --file, piped stdin).
  • Scripting: (mentat.store/q db query arg1 …) binds :in inputs, and the shared Datomic model suite now covers inputs, history patterns, cas and retractEntity on every backend.
  • mentat::options_from_json: one parser for the options JSON, shared by the CLI and both extensions. Store::q_explain_temporal.

Changed

  • Aggregates follow Datalog set semantics on PostgreSQL (as Datomic and the embedded backend already did): sum, avg and count of a value variable aggregate the set of bindings. (sum ?heads) over heads 1, 1, 1, 3 is now 4 (was 6); add :with ?e for the old result. :with is now honoured (it was ignored), count-distinct works (it was rejected), and sum/avg over doubles work.
  • The embedded store’s schema is version 3. Older stores upgrade once on open (about 20 s for 10M datoms): a covering history index, persisted partition high-water marks, and the value indexes above.
  • Embedded SQLite connections use file-backed temp storage (temp_store=1, was 2): in-memory sorting made repeated large GROUP BYs on one connection slower every time. MENTAT_TEMP_STORE=2 restores it (e.g. for Android).
  • Builds in this repo compile the bundled SQLite without SQLITE_ENABLE_MEMORY_MANAGEMENT (.cargo/config.toml), which put every connection behind one global page-cache lock. Every connection also sets mmap_size (MENTAT_MMAP_SIZE, default 1 GiB), which helps downstream builds that don’t use this repo’s config.
  • The mentat.max_result_rows and mentat.temp_file_limit errors now name the setting and suggest a value.

Fixed

  • Embedded: a transaction of 5,461 or more datoms panicked; an interrupted query panicked and left the Store’s lock poisoned; as-of queries took over 20 s (no history index); Store::open scanned the whole transaction log (16 s at 10M datoms, now 50 ms); concurrent readers ran slower than one.
  • PostgreSQL: get-else always returned the default, missing? matched everything, and the attribute pushdown never fired, because attribute idents were looked up as ::ns/attr.
  • PostgreSQL: a 5-place [?e ?a ?v ?tx ?added] pattern now reads the transaction log (it saw only current state unless the caller also passed {"history": true}); an integer :in input for a ref attribute’s value now matches the entity (it was compared as a long and matched nothing); and a declared :in binding with no input value is an error instead of being silently ignored. mentatd’s :q path had been dropping query args because of that last one.
  • Embedded: under constant readers the SQLite WAL grew without bound (442 MB after 120 s of 32 readers + 1 writer; reads slowed 20x on a 1.37 GB WAL). After a commit, a writer now restarts a WAL past MENTAT_WAL_RESTART_BYTES (default 64 MiB).
  • SQLite and DuckDB extensions: every call reopened the store, so bulk loads were quadratic and a point lookup took 466 ms at 1M datoms (5 s at 10M). Each thread now caches its open stores per path and reopens one when another connection has committed: 0.63 ms at 1M datoms, 1.1 ms at 10M.

Performance (p50, v1.9.0 → 1.10.0, c7i.8xlarge)

s (1M datoms) m (10M datoms)
PG point lookup 0.43 → 0.19 ms 1.65 → 0.19 ms
PG count by state (q3) 112 → 9.2 ms 1428 → 58 ms
PG read mix, 32 clients 27.6K → 32.6K ops/s 12.5K → 30.7K ops/s
Embedded point lookup 0.070 → 0.019 ms 0.515 → 0.019 ms
Embedded count (q3) 102 → 31 ms 1352 → 372 ms
Embedded as-of 20.8 s → 88 ms >20 s → 1.04 s
Embedded read mix, 32 clients 6.8 → 2,095 ops/s timed out → 157 ops/s

Costs: the embedded store is about 28% larger and bulk loads about 27% slower, from the new history and value indexes.

Known gaps

  • :db/retractEntity doesn’t retract references to the entity (Datomic does), and cas/retractEntity don’t take lookup refs, on either backend.
  • The embedded scripting q still refuses an as-of/since db.
  • PostgreSQL mentat_explain / mentat_query_sql ignore collection :in inputs, and a 5-place history pattern inside not or a rule body still reads current state.
  • edn_q_rows builds the whole result within one call (it avoids the single JSONB value limit, but isn’t row-at-a-time streaming).
  • Quack’s listen backlog (5) limits the DuckDB server under bursts; the SQLite and DuckDB extensions cache one store per thread, so a Quack server can hold up to ~128 connections per store.

Upgrade

ALTER EXTENSION pg_mentat UPDATE TO '1.10.0'; from 1.9.0 or 1.9.1 (1.9.0 goes through 1.9.1’s pg_dump fix). It builds an AVET index on each current_<type> table, so on a large store it takes a while and blocks writes while it runs. The embedded store upgrades itself on first open (a one-time index build; about 20 s at 10M datoms).

[1.9.1] — pg_dump now includes mentat’s data

Fixed

  • PostgreSQL: a pg_dump of a database using pg_mentat restored every store EMPTY. All mentat tables are extension members, and pg_dump dumps the data of an extension member only when the table is registered with pg_extension_config_dump(). None were, so a logical backup carried the schema and none of the datoms, transactions, attributes or idents. Found on a production database whose nightly pg_dump held data for 0 of 28 mentat tables. 1.9.1 registers every member table and sequence (install: sql/27_dump_config.sql; upgrade: pg_mentat--1.9.0--1.9.1.sql). The six tables CREATE EXTENSION seeds are registered with a filter that excludes the seed rows, so pg_restore into a fresh database does not collide with them. Tested: a store created on 1.9.0, upgraded in place to 1.9.1, dumped and restored into a new database answers the same queries and accepts new writes. Physical backups (pg_basebackup, WAL archiving) were never affected.

Upgrade

ALTER EXTENSION pg_mentat UPDATE TO '1.9.1'; — no schema or data change. Take a fresh pg_dump afterwards; dumps made before the upgrade do not contain the mentat data.

[1.9.0] — one SQL surface (edn_*) on SQLite, PostgreSQL and DuckDB

Changed — renamed functions

  • The core SQL functions are now edn_t (transact), edn_q (query), edn_pull and edn_eval (mino scripting) on every backend.
  • PostgreSQL: mentat_transact, mentat_query, mentat_pull and mentat_eval still work as deprecated SQL wrappers around the new names and will be removed in a future major release. ALTER EXTENSION pg_mentat UPDATE TO '1.9.0' adds the new names to a 1.8.0 install (tested both with and without the script feature). The mentat.q / mentat.t / mentat.pull aliases now call the new names, and mentat_query’s inputs argument now defaults to '{}'.
  • DuckDB (breaking): mentat_transact and mentat_query are gone, replaced by edn_t and edn_q with no aliases (the extension hadn’t been published yet). The placeholder mentat_hello() is removed.
  • DuckDB (breaking): edn_q now returns strings as plain text (Alice). Before, it returned them with the quotes included ("Alice"), so joins against native VARCHAR columns found no matches. Keywords keep their leading colon.

Added

  • SQLite loadable extension (crates/sqlite/ext, libmentat_sqlite.so): the same four functions for any SQLite host (.load in the sqlite3 CLI, load_extension in Python, and so on). edn_q returns pg_mentat’s JSON shape, so json_each(edn_q(...)) joins against native tables. The functions are SQLITE_DIRECTONLY, and host SQLite 3.30 or newer is required. The extension embeds its own copy of the engine and SQLite and exports only its init symbol.
  • DuckDB: edn_pull (JSON in pg_mentat’s shape) and edn_eval (sandboxed mino, on by default) are new. edn_q’s options argument now works; before, it was read and ignored.
  • Options JSON on every backend: {"inputs": [...]} binds :in forms by position, {"asOf": tx} and {"since": tx} query the database as of or since a transaction. Inputs can mix scalars with collection, tuple and relation bindings (:in ?age [?name ...]) through the new QueryInputs::merge.
  • mentat::script::Interpreter::with_default_path(path): a sandboxed interpreter where (mentat.store/open) with no argument opens path.

Fixed

  • PostgreSQL: reactive subscription triggers called mentat_query with one argument, which no function accepted, so every subscription failed when its trigger fired. They now call edn_q(query, '{}').
  • PostgreSQL: mentat_query_stats and mentat_slow_queries now count calls to the new edn_* names.
  • mentatd sends the new function names.

[1.8.0] — 2026-09-26 — DuckDB backend + embedded feature parity

Added

  • DuckDB extension (crates/duckdb, mentat_duckdb) — a third way to use mentat, alongside the embedded SQLite library/CLI and the PostgreSQL extension. Load it into DuckDB (LOAD mentat;) and query the embedded mentat store from SQL: mentat_transact(db_path, edn) (scalar, returns a JSON tx-report) and mentat_query(db_path, query, inputs) (table function, returns rows), so Datalog results join against native DuckDB tables. Built with duckdb-rs against DuckDB v1.5.5 (pinned via the unstable C API); the first cut embeds the SQLite store and a DuckDB-native storage backend is future work. See docs/duckdb-extension-plan.md.
  • Embedded (SQLite) temporal + input parity with the PostgreSQL backend:
    • History patterns [?e ?a ?v ?tx ?added] in :where, over the transactions log (assertions and retractions).
    • Historical queries: Store::q_once_as_of(tx, …) / Store::q_once_since(tx, …) (and the Conn equivalents) evaluate a query against the database as of, or since, a transaction.
    • Non-scalar :in bindings — collection [?x ...], tuple [?a ?b], and relation [[?a ?b]] inputs (previously accepted by the parser but dropped).
  • mentat-script shared crate (crates/script) — the mino mentat.store/* scripting layer, previously duplicated in the SQLite and PostgreSQL backends, is now one crate behind a ScriptBackend trait, exercised by a single model test suite that runs against both backends (and an in-memory fake). The sandboxed, resource-limited mentat_eval on the PostgreSQL side is unchanged.

Changed

  • The scripting transact tx-report now carries :mentat.store/db-after on both backends (previously PostgreSQL only) — the Datomic-faithful shape.

[1.7.0] — 2026-09-26 — merged repository

Changed

One repository, two storage backends. The mentat (embedded, SQLite) and pg_mentat (PostgreSQL extension) projects, which already shared their EDN front-end and Datalog query engine, are now one repository at codeberg.org/gregburd/mentat. The tree is a single Cargo workspace under crates/: the shared front-end (edn, core-traits, core), the mino scripting interpreter (mino, maintained here now that upstream mino is archived), the SQLite side (crates/sqlite/*), and the PostgreSQL side (crates/pg/pg_mentat, crates/pg/mentatd). One toolchain, one lockfile, one nix flake, and one CI gate (Forgejo on Codeberg; GitHub Actions on the mirror publishes). pg_mentat’s full git history is preserved under crates/pg/.

Entries below this line and dated on/before 2026-09-24 are the pg_mentat history; the condensed embedded-mentat (0.x) history follows at the end.

Added

  • Embedded (SQLite) transaction functions :db.fn/cas and :db/retractEntity (both :db.fn/* and :db/* spellings). cas reads the current value inside the write transaction and aborts the whole transaction with a typed CasMismatch error on mismatch; retractEntity retracts every datom with the entity as subject and recurses through :db/isComponent children. The PostgreSQL backend already had these.
  • Reconciled front-end grammar shared by both backends: 5-place history patterns [?e ?a ?v ?tx ?added], source variables in :in (:in $ …), plain (non-namespaced) keywords as pattern values ([?e :status :done]), and :rules/:with [[…]] rule definitions. The embedded algebrizer accepts the new shapes it can serve and returns a clear, typed error for the ones still PostgreSQL-only (history/as-of q, non-scalar :in bindings — planned for the embedded side in a later release).
  • mino refreshed to upstream’s final release (9c65bb50), now maintained in this repository: #uuid reads to a real UUID value and round-trips, #inst prints as a reader literal, read-string, classed/keyword catch, regex lookahead, a distinct delay type, a pluggable store backend seam, and new core.clj helpers. BigDecimal literals remain deferred.

Fixed

  • Deeply nested EDN no longer crashes the server. The shared parser rejects input nested deeper than 256 levels before recursing, so a hostile or accidental deep value can no longer overflow the backend’s stack (it returns a parse error). This shipped for the extension in 1.6.2 and is now in the merged front-end.
  • mino is stack-safe on adversarial data: the reader, printer, structural equality, hashing, and comparison are bounded (depth caps, an iterative equality worklist, printer cycle detection) and the garbage collector marks iteratively, so deeply nested or self-referential values error cleanly instead of aborting the process. Interpreter recursion is trampolined (constant stack for loop/recur and mutual recursion).
  • Reachable unimplemented!() panics in the embedded query engine (query algebrizer, projector, and the destination cache) are now typed errors or documented-unreachable arms, so an unsupported query shape returns an error rather than panicking.

Security

mentat_eval is now sandboxed and resource-limited. The optional mino scripting surface (mentat_eval(TEXT), behind the script cargo feature, added in 1.6.0) built its interpreter with mino_rs::Interpreter::new(), which installs host-filesystem primitives (slurp, spit, rm-rf, mkdir-p, file-exists?) and a file-backed store, and imposed no CPU, memory, or stack limit. Because the extension issues no REVOKE, EXECUTE on mentat_eval is granted to PUBLIC, so any role that could call it — in a build that enabled --features script — could read or delete files as the server’s OS user, pin a backend forever with (loop [] (recur)), exhaust memory with (range 1e11), or crash the whole server into recovery with deep non-tail recursion. No shipped artifact was affected: script is off by default and none of the flake, Dockerfile, or release workflow in the 1.6.0–1.6.2 builds enabled it — only someone who built with --features script themselves was exposed.

mentat_eval now builds a sandboxed interpreter (mino_rs::Interpreter::sandboxed()): the language, regex, bignum, atoms and the SPI-backed mentat.store/* surface remain, but every host-filesystem prim and the file-backed store are absent (unbound). Three PGC_SUSET GUCs bound each call — mentat.script_max_steps (10,000,000), mentat.script_max_heap_bytes (64 MiB), mentat.script_max_depth (2000) — throwing an :eval/limit error instead of hanging, exhausting memory, or overflowing the stack; being PGC_SUSET, an ordinary role cannot raise them for its own session. An interrupt check hook wires statement_timeout and pg_cancel_backend() into the interpreter, and stack_is_too_deep() as a second line against runaway recursion. mentat_eval remains intentionally callable by every role (no REVOKE) and is deliberately not SECURITY DEFINER: a script runs through SPI as the calling role, so it reaches only the stores that role can already query with mentat_query/mentat_transact, gaining no privilege.

[1.6.2] - 2026-09-24

Security

Deeply nested EDN crashed the server. The EDN parser recursed once per nesting level, so input nested a few thousand levels deep overflowed the backend’s stack. The backend died with SIGSEGV and the postmaster terminated every server process and ran crash recovery, disconnecting all clients. Any role that can call mentat_query, mentat_transact, mentat_pull, mentat_pull_many, or cast text to mentat.edn could trigger it; in principle that is every role (in practice only superusers before this release, because of the temp_file_limit bug below). For example, a query whose :where clause held a vector nested 3,000 deep. All earlier releases are affected.

The parser now rejects input nested deeper than 256 levels, before parsing, with an ordinary error (expected nesting depth at most 256). The mentat.edn type’s existing 100-level limit is now also checked before parsing; it was previously checked afterwards, too late to help. Real queries and transactions are unaffected: the deepest one in the test suite nests 5 levels. Regression tests cover each entry point above, and the fix is in the shared edn crate, so the embedded mentat crate gets it too.

Fixed

mentat_query failed for every role that isn’t a superuser. Each query sets a few transaction-local limits and planner hints first, including temp_file_limit, which only superusers may set. Since 1.5.3 that was done in a way that turned the refusal into an error, so any non-superuser got ERROR: permission denied to set parameter "temp_file_limit" on every query. On PostgreSQL 13 and 14 superusers got it too (15+ checks parameter ACLs, which superusers pass), so on those versions the query path did not work at all. The test suite runs as a superuser and CI tests only PostgreSQL 16, so neither showed it. Each limit is now set with the caller’s own privilege, like SET LOCAL; one the caller may not set is skipped instead of failing the query. mentat.temp_file_limit therefore only takes effect for superusers; for other roles, set temp_file_limit itself (as a superuser, per role or database). New test: an ordinary role runs mentat_query.

Instants from (max ?x), (min ?x) and other decoded values were off by the server’s UTC offset. The value decoder formatted timestamptz in the session TimeZone but appended a literal Z, so on a non-UTC server a stored 2026-09-08T12:00:00Z came back as 2026-09-08T08:00:00Z (America/New_York). Instants are now converted to UTC before formatting. Found because the 1.6.1 regression test fails on any non-UTC machine; a new test pins two non-UTC session zones.

Upgrade

ALTER EXTENSION pg_mentat UPDATE TO '1.6.2'; — no schema changes.

[1.6.1] - 2026-09-08

Fixed

(min ?x) / (max ?x) failed on every non-numeric value type. The aggregate SQL builder unconditionally cast the decoded value to ::NUMERIC, which is correct for SUM/AVG but wrong for MIN/MAX — those are defined on any ordered type. As a result the two aggregates worked only on long- and ref-valued attributes and raised a raw PostgreSQL cast error (invalid input syntax for type numeric: "...") on instants, strings, keywords, booleans, doubles (hex-encoded behind a d: prefix), uuids, and bytes. Reported against 1.6.0 by pg.ddx.io, where two API endpoints used (max ?at) over an instant attribute (one returned HTTP 500; the other silently fell back to the current clock, masking the error).

MIN/MAX now order without the numeric cast:

  • long/ref keep the numeric comparison, so (max ?n) returns the numeric maximum (61 > 9), not the lexicographic one ("9" > "61") — the declared :db/valueType is known at plan time and selects this arm.
  • every other type orders on the decoded text, whose rendering is already order-preserving by design (instants fixed-width UTC, doubles hex-encoded for monotonic sort), so (max ?at) returns the newest instant, (max ?t) the lexicographic-max string, etc.

SUM/AVG (numeric by definition) are unchanged.

Added a regression test per value type in aggregate_tests.rs, including a "9" vs "61" case that pins the long behaviour against future regression. Qualified with the full cargo pgrx test suite (1870 tests) green on PG 16.

[1.6.0] - 2026-08-29

Added

Optional Datomic-in-Clojure scripting surface (mentat.store/*), embedding the pure-Rust mino-rs interpreter. Built with --features script, pg_mentat exposes a new mentat_eval(script TEXT) -> TEXT SQL function that evaluates a mino (Clojure-dialect) script and returns its result as EDN. This mirrors the scripting layer added to the standalone mentat crate, adapted to a Postgres backend: the interpreter is a plain Rust-heap value built, run, and dropped within one function call, and every mentat.store/* primitive calls pg_mentat’s existing engine functions (transact, query, pull, entity) rather than owning any connection.

The mentat.store/* namespace implements the Datomic value-and-time model:

  • open — a conn handle (there is exactly one database).
  • db — an immutable database value (a map carrying :basis-tx, :as-of, :since), not a connection. All reads take a db value.
  • transact — commits; returns a tx report (:tx-id, :tempids, :db-after).
  • with — a pure speculative db -> db' that runs the full pipeline in a savepoint and rolls back (does not commit).
  • q / q-once — arbitrary Datalog. Because pg_mentat’s mentat_query accepts asOf/since inputs, full q runs faithfully against a historical basis — an advantage over the standalone Mentat crate, whose algebrizer has no as-of query rewrite.
  • pull, entity, read, entities, datoms — reads over a db value.
  • as-of / since — temporal db values.
  • Eids resolve from integers, ident keywords, or [:attr val] lookup-refs.

JSON engine results are converted back to mino values recursively, restoring keyword typing and preserving nested pull maps. The feature is off by default: a default-built module pulls and costs nothing, and the 1.5.7 -> 1.6.0 upgrade edge is a no-op for default installs.

Covered by 21 #[pg_test] integration tests (against a real PostgreSQL 16 backend) and 10 pure value-conversion unit tests.

[1.5.7] - 2026-07-07

Fixed

The 1.5.6 upgrade migration was unsafe for stores whose sequences had already overflowed their bands, and it could not remediate the resulting entid collisions. A production operator on 1.5.5 correctly held the 1.5.6 bump after finding their partition_user_seq (70M) and partition_tx_seq (26.9M) long ago overflowed the old [1e4,1e6) / [1e6,2e6) bands (the sequences were unbounded to bigint-max, so they never failed loud), with 1,568 realized entid collisions — entids used as BOTH a transaction and a user/schema entity.

  • Unsafe migration. The 1.5.5→1.5.6 migration did ALTER SEQUENCE … MAXVALUE <fixed ceiling>, which errors and aborts the whole ALTER EXTENSION when the sequence’s last_value already exceeds that ceiling (RESTART value (N) cannot be greater than MAXVALUE (m)). 1.5.7 ships a direct 1.5.5→1.5.7 upgrade edge (PostgreSQL picks the shortest path, bypassing the broken 1.5.6 step) and a 1.5.6→1.5.7 edge. Both bound each sequence to GREATEST(intended_ceiling, current_head) — never below the live head — so an overflowed store keeps an effectively-unbounded ceiling (no break) while in-band stores still get the fail-loud ceiling. A NOTICE flags any partition left unbounded.
  • Collision diagnostics + repair. New mentat.entid_collision_report(), mentat.entid_collision_count(), and (opt-in, dry-run by default) mentat.repair_entid_collisions(dry_run, store). The repair renumbers the colliding non-tx entities into fresh user-band ids — rewriting e, a, and incoming ref v across the nine log tables + nine current projection tables, and the schema/idents catalogs — while the transaction keeps its id (its id anchors the tx column and basis-t / :as-of monotonicity, so it must not move). Verify with entid_collision_count() = 0.
  • New-layout genesis overlap. 1.5.6’s new user band started at exactly 1000000, the same value as the genesis-transaction sentinel, so the first user entity on a fresh store collided with the genesis tx. The user band now starts at 1000001, leaving 1000000 as the lone genesis sentinel. (Fresh 1.5.7 installs report zero collisions; a store created fresh on 1.5.6 has this one benign collision, which repair_entid_collisions clears.)

Upgrading

ALTER EXTENSION pg_mentat UPDATE TO '1.5.7';

Safe on any sequence position (never lowers a MAXVALUE below the live head). After upgrading, check for pre-existing partition overlap and repair if needed:

SELECT mentat.entid_collision_count();              -- 0 == healthy
SELECT * FROM mentat.entid_collision_report();      -- inspect collisions
SELECT mentat.repair_entid_collisions(true);        -- dry run (count only)
-- back up first, then, inside your own transaction:
SELECT mentat.repair_entid_collisions(false);       -- perform the repair
SELECT mentat.entid_collision_count();              -- confirm 0

[1.5.6] - 2026-07-06

Fixed

  • Entity-id partition sequences are now bounded, and the tx band is no longer a time bomb. The three partition sequences (partition_db_seq, partition_user_seq, partition_tx_seq) shipped with only START WITH and no MAXVALUE, so an exhausted partition silently issued ids that collided with the next partition’s space – a latent entid-space-corruption hazard. Worse, the tx band was [1000000, 2000000): one tx id is consumed per mentat.t, so a write-heavy store (e.g. ~33k tx/day) exhausted it in weeks and then mentat.t began issuing ids outside its band.

    Fresh installs use a new, disjoint, generous layout, and every partition sequence is bounded to its band with MINVALUE/MAXVALUE so exhaustion fails loud (nextval: reached maximum value of sequence) instead of colliding: | partition | band | sequence bound | |—|—|—| | db.part/db | [0, 1e6) | MAXVALUE 999999 | | db.part/user | [1e6, 1e12) | MAXVALUE 999999999999 | | db.part/tx | [1e12, 2e12) | MAXVALUE 1999999999999 |

    This also fixes the intermittent concurrency_tests failures (multi_partition_interleaved_allocation, allocate_entid_uniqueness_per_partition): the bands are now disjoint by construction, and the concurrency test setup resets the sequences so the shared-instance / non-transactional-sequence drift between pgrx tests can no longer perturb the assertions.

Upgrading

ALTER EXTENSION pg_mentat UPDATE TO '1.5.6';

Existing stores cannot be re-banded in place (their user ids already sit directly below the old tx band), so the migration does the safe subset: it bounds db/user at their existing band ceilings (fail-loud on exhaustion) and raises the tx ceiling far upward (to 1e12), removing the exhaustion time bomb without moving any live id. No id is relocated; data is untouched. Fresh installs get the full new layout.

[1.5.5] - 2026-07-06

Fixed

  • VAET index on all value tables. The transact / lookup-ref resolution probe SELECT e ... WHERE store_id=? AND a=? AND v=? (resolve an entity id from a known attribute+value) fires once per resolvable ref/upsert value inside mentat.t, on every value type. Only datoms_ref_new and datoms_keyword_new shipped a VAET index (store_id, v, a, e, tx); the other seven value tables (text, long, double, instant, uuid, bytes, boolean) resolved this by scanning the AEVT index on (store_id, a) and filtering by v. On a high-fanout attribute (millions of rows per (store_id, a)) that scan is pathological. A production operator measured ~30x on a 1.1M-row attribute; the probe runs inside mentat.t, so it directly dominated write-path latency. All nine value tables now carry the VAET index. The value column is already part of each table’s primary key, so there is no new index-row-width risk for text or bytes. (Reported with measurements by the agora / pg.ddx.io operator.)
  • Added a schema_introspection regression test asserting every value table has its VAET index, so the gap cannot silently return.

Upgrading

ALTER EXTENSION pg_mentat UPDATE TO '1.5.5';

The migration creates the seven missing VAET indexes with CREATE INDEX IF NOT EXISTS. ALTER EXTENSION runs in a transaction, so these cannot be CONCURRENTLY and a plain CREATE INDEX takes a write- blocking SHARE lock for the build. On installs with large existing value tables under heavy ingest, build the indexes CONCURRENTLY out-of-band first (then the migration’s IF NOT EXISTS builds are no-ops):

CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_datoms_text_new_vaet
    ON mentat.datoms_text_new (store_id, v, a, e, tx) WHERE added;
-- repeat for long/double/instant/uuid/bytes/boolean as your data warrants
ALTER EXTENSION pg_mentat UPDATE TO '1.5.5';

[1.5.4] - 2026-06-29

Fixed

  • clippy::let_and_return in lookup_by_ident (the 1.5.3 read-only-SPI conversion left a redundant let result = ...; result). The 1.5.3 tag’s clippy -D warnings CI job failed on this; the test suites and builds passed. CI-only — the compiled module is functionally identical to 1.5.3.

Upgrading

ALTER EXTENSION pg_mentat UPDATE TO '1.5.4';

No-op migration (no schema change).

[1.5.3] - 2026-06-29

Fixed

Completes and corrects the hot-standby read path begun in 1.5.2. No schema or SQL-object change; compiled-module only.

  • Restored mentat.q / mentat_query on the primary. 1.5.2 routed the query path through read-only SPI, which flips the transaction read-only before apply_optimizer_hints runs; its SET LOCAL resource hints (issued through SPI) were then rejected with “SET is not allowed in a non-volatile function”, breaking every Datalog query. The resource-limit GUCs (statement_timeout, temp_file_limit, enable_seqscan, work_mem) are now set via pg_sys::set_config_option with GUC_ACTION_LOCAL instead of a SQL SET, which is permitted in a read-only / recovery transaction and reverts at transaction end.
  • Completed standby coverage of the read path. The remaining read-side store-id / lookup resolutions now use read-only SPI, so they run on a hot-standby too: mentat.pull, mentat.entity, :as-of / :since time-travel queries, mentat.lookup_by_ident, and the has_<ext>() extension-detection helpers. Write paths (transactions, excision, entid allocation, cache-generation bump) keep mutable SPI; they cannot run on a standby regardless.

Upgrading

ALTER EXTENSION pg_mentat UPDATE TO '1.5.3';

No-op migration (no schema change); it exists only so the ALTER EXTENSION command succeeds and the recompiled module is picked up.

[1.5.2] - 2026-06-29

Changed

Makes the Datalog read query path (mentat_query / mentat.q / the view helpers) run on a PostgreSQL hot-standby (read-only replica). The read path previously used pgrx’s mutable SPI (Spi::connect_mut), which assigns a transaction id and fails on a standby with “cannot assign TransactionIds during recovery”. It now uses read-only SPI.

Compiled-module only: no schema or SQL-object change. (Note: 1.5.2’s resource-hint handling broke mentat.q on the primary; fixed in 1.5.3 — prefer 1.5.3.)

Upgrading

ALTER EXTENSION pg_mentat UPDATE TO '1.5.2';

[1.5.1] - 2026-06-18

CI / maintenance release

No schema, SQL-object, or query/transaction behavior changes relative to 1.5.0; the recompiled extension is functionally identical. This release greens the CI pipeline and refreshes tooling.

Changed

  • CI Rust toolchain pinned to 1.90.0 (was 1.88.0). pgrx 0.17 uses NonNull::from_mut, stable since 1.89, so 1.88 failed to compile pgrx.
  • cargo fmt --all applied across the workspace (the CI format check had drifted on ~115 files).
  • tokio-postgres → 0.7.18 and postgres-protocol → 0.6.12 in the mentatd client, closing RUSTSEC-2026-0178/0179/0180 (DoS). These are not in the PostgreSQL extension’s runtime.
  • Added per-crate license = "Apache-2.0" to mentat_core and core_traits.

Fixed (CI)

  • 1.90 clippy lints in production code (doc_overindented_list_items, neg_cmp_op_on_partial_ord; the latter keeps NaN rejection in the geom-within radius check).
  • GitHub docs workflow: version-pinned the mdBook download URL; enabled GitHub Pages on the mirror.
  • Container workflow: build Dockerfile (not the nonexistent Containerfile), added the demo.sql the image references, glob the versioned base SQL, and test by querying the running image (the lean runtime image has no Rust toolchain).
  • cargo pgrx test jobs (GitHub and Nix): install into a writable pgrx-managed PostgreSQL rather than a root-owned system / read-only nix-store one, which had caused every test to abort.
  • Security-audit job: added deny.toml (license allow-list + advisory triage) and --ignore for the four advisories with no clean fix (transitive via pgrx / prometheus).
  • Optional-extension test suites (pgvector, rum, fuzzystrmatch, pg_trgm): route the speculative CREATE EXTENSION through a subtransaction helper so a missing third-party extension skips cleanly instead of poisoning the test transaction.
  • Nix flake: export -f the dev-shell helpers; writable CARGO_HOME / PGDATA; provide pg_config via postgresql.pg_config; add readline/zlib/icu .dev outputs for from-source PG builds.

Upgrading

ALTER EXTENSION pg_mentat UPDATE TO '1.5.1';

The migration is a no-op (no schema change); it exists only so the ALTER EXTENSION command succeeds.

The “Append-Only Datom Log” release

Makes the datom log a true immutable append-only log and adds the Datomic-compatible :db/noHistory attribute class. Driven by the same production feedback as 1.4.0: the instant-datom bloat was rooted in (a) an in-place added-flip on retraction that violated the datom model and (b) keeping full history of monotonic timestamps that change every sync. Both are now fixed structurally.

This is a storage-model change with a required upgrade migration (see Upgrading). Current-time query results are unchanged; the internal representation of history is not.

Changed — append-only datom log

  • Retraction no longer flips the prior assertion in place. A retraction is now a new immutable (e, a, v, tx, false) datom; the original (e, a, v, tx0, true) row is preserved unchanged. This resolves the 1.4.0 “redundant retraction row” Known Issue at its root — the in-place flip was the bug; appending the retraction is the correct Datomic behavior.
  • Current-time queries read a maintained current-state projection (nine mentat.current_<type> tables) instead of resolving latest-tx-wins over the full log. :as-of / :since / history queries continue to read the append-only log.
  • fillfactor 85/90 → 100 on the nine datoms_*_new log tables. Append-only tables never update in place, so the reserved HOT-update space the old flip required is pure waste.

Added — current-state projection (the read path)

  • Nine mentat.current_<type> tables holding only live datoms, maintained in lock-step with the log inside each transaction.
  • mentat.current_datoms view (union over the nine, legacy datoms-shaped columns) for callers needing current state.
  • mentat.rebuild_current_projection(store) — repopulate from the log (used by the upgrade and for recovery).
  • mentat.verify_current_projection(store) — returns the count of rows where the projection disagrees with a fresh latest-tx-wins resolution of the log; 0 means consistent. Used as the cutover safety gate and in tests.

Added — :db/noHistory attributes

  • Datomic-compatible :db/noHistory true attribute flag. A noHistory attribute keeps only the current value: each assertion physically replaces the prior value in the log and projection instead of appending a retraction + assertion. The structural fix for monotonic-attribute bloat (:last-seen / :observed-at): 10 updates leave 1 log row, not ~20.
  • Per-attribute and per-cardinality (one and many). Current-time queries behave identically to a normal attribute; :as-of sees only the current value (the trade for zero bloat).

Fixed (exposed by the conversion)

  • :db.fn/cas read the current value via datoms WHERE added=true, which in the append-only model returns superseded historical assertions too. CAS now reads mentat.current_datoms.
  • batch_insert_datoms dedups by full PK (e,a,v,tx,added): CAS queues a retraction and the cardinality-one replace path independently queues the same one; a single INSERT ... ON CONFLICT cannot list a key twice.
  • is_duplicate_cardinality_many reads the projection (presence == live) rather than an added=true log scan.
  • pull, (fulltext), and the extension-search where-fns ((fuzzy-match)/pg_tre, (similar-to)/pg_trgm, (rum-fulltext)/rum, (infer-near)/pg_infer) read the current-state projection, so they no longer return values that were replaced or retracted. The extension index helpers (create_trgm_index, create_rum_fulltext_index, …) now build on mentat.current_text rather than the log table.
  • Reverse-reference (:ns/_attr) and recursive-reference pull traversals read the projection instead of the append-only log.

Fixed — query/transaction correctness (fail-loud)

  • (pull ?e [...]) inside a :find clause is now implemented; it previously produced a NULL column. The result nests as a JSON object.
  • The mentat.edn type’s text input parsed nothing (returned NULL for all valid EDN); it now round-trips raw EDN via an explicit I/O impl.
  • A scalar supplied through :in and used only in a predicate (e.g. [(>= ?age ?min)]) was rejected as unbound; it now binds correctly.
  • An unknown transaction op, a malformed assertion, an incomplete schema-attribute definition, an unbound :find variable, and an unknown attribute in a query now all fail loud with a :db.error/* message instead of silently returning wrong or empty results. A bare :db/ident (naming a non-attribute entity, e.g. an enum value) is no longer mis-flagged as an incomplete attribute.

Tests

  • current_projection_tests (8), no_history_tests (6) — all green.
  • history_tests::test_hi_many_retract_history (failing on 1.4.0) now passes.
  • Full suite is green: 1829 passed, 0 failed. The pre-existing test debt (108 failures across ~30 suites — obsolete idx_datoms_* introspection from the storage redesign plus scattered functional rot) has been cleared, partly by retargeting stale tests to the narrow-table / projection model and partly by the fail-loud fixes above (several failures were correct tests guarding real bugs).
  • The 1.4.0 → 1.5.0 in-place upgrade is qualified end to end: install 1.4.0, load data, ALTER EXTENSION … UPDATE TO '1.5.0', then verify_current_projection(0) = 0 and current-time queries return results identical to pre-upgrade.

Upgrading

ALTER EXTENSION pg_mentat UPDATE TO '1.5.0';

The migration creates the projection tables, retunes the log tables to fillfactor=100, and — required — runs mentat.rebuild_current_projection(0) to populate the projection from the existing log. Without that population step, current-time queries return nothing. The migration handles it automatically; if you build a store by other means, call rebuild_current_projection yourself.

Pre-1.5.0 history was flip-based; those rows remain as-is. The projection is rebuilt by latest-tx-wins resolution, which is correct against both flip-era and append-only-era history. All retractions going forward are appended, never flipped.

[1.4.0] - 2026-06-16

The “Production Throughput & Bloat” release

Driven by production feedback from an 82 GB store used as a community-stats identity backbone. Focus: mentat.t() ingest throughput, narrow-table autovacuum, and a cheap live-projection read path. No new query surface; no breaking changes.

Performance

  • Cardinality-one assertion fast path. mentat.t() previously ran a 9-way UNION ALL probe per cardinality-one datom to find the current value (to decide assert / replace / skip). Because the new value’s type is always known and a cardinality-one attribute’s type is fixed, the current value lives in exactly one narrow table. The probe is now a single indexed lookup on that table’s (store_id, e, a, tx DESC) WHERE added covering index. Measured ~1.8x speedup (6.2 s -> 3.4 s) on a 2,000-call cardinality-one re-assertion microbenchmark. The residual per-call cost is fixed tx-allocation overhead, amortized by batching more facts per t().

Operations

  • Autovacuum retune on all narrow tables + transactions. The previous autovacuum_vacuum_scale_factor = 0.05 (and the PG default 0.2) effectively stops triggering on large tables, so they bloat without bound — most visibly datoms_instant_new, where monotonic attributes (:first-seen / :last-seen / :observed-at) are re-asserted every sync. All nine datoms_*_new tables and mentat.transactions now ship with scale_factor = 0 + a fixed threshold = 50000, so autovacuum fires on a constant dead-tuple count regardless of table size. Applied by CREATE EXTENSION and the 1.3.0->1.4.0 upgrade.

Added — operational + read-path accessors

  • mentat.attr_id(':ns/name') — resolve an attribute keyword to its entid for use in SQL / views (STABLE), so generated viewdefs read a = mentat.attr_id(':person/name') instead of a = 1308861.
  • mentat.current(e, a) and mentat.current(e, ':ns/name') — index-backed “current value of attribute A for entity E” as TEXT, dispatching on the declared value type so only one narrow table is touched. Replaces per-query DISTINCT ON / LATERAL fan-out in consumer views.
  • mentat.attribute_health() — per-attribute live datom count plus the backing narrow table’s dead-tuple %, so operators can alert on bloat before it bites.

Documentation

  • New docs/src/operations.md: throughput (batching strategy, the cardinality-one fast path, idempotent-reassert no-op), bloat (the default-scale-factor trap, the 1.4.0 autovacuum defaults, reclaiming existing bloat, monitoring with attribute_health()), and the live projection (mentat.current / mentat.attr_id, a maintained current-state partial-index pattern). Explicitly addresses the “is this an auto-indexing problem?” question: it is not — the indexes already exist; the costs are per-tx overhead, history resolution, and bloat.

Fixed

  • Removed a dead insert_typed_datom function (superseded by the batch-insert path) to restore the zero-warnings build.

Known issue (reported, not yet changed — needs a semantics decision)

  • A cardinality-one replace writes a redundant (e, a, old_v, tx, false) retraction datom in addition to flipping the original assertion row’s added flag in place. This double-counts retraction churn (one extra dead row per replace) and contributes to the instant-datom bloat above. Fixing it changes history-replay semantics (whether the tx log carries an explicit retraction datom), so it is deliberately left for a maintainer decision rather than changed silently. Tracked for 1.5.0.

Tests

  • operational_accessors_tests (6 #[pg_test]): attr_id resolution, current() latest-value + NULL-absent + post-replace, the cardinality-one fast-path correctness (exactly one live datom after repeated replaces), idempotent-reassert no-churn, and attribute_health() counts.
  • Regression: comprehensive_upsert_tests (17/17), comprehensive_retract_tests (22/22), cross_entity_tests (14/14) all green — the fast path preserves cardinality / upsert / retract semantics.

Upgrading

ALTER EXTENSION pg_mentat UPDATE TO '1.4.0';

The upgrade retunes autovacuum and installs the new accessors. To reclaim existing bloat (storage params only affect future triggering), run VACUUM FULL or pg_repack on the affected tables — see docs/src/operations.md.

[1.3.0] - 2026-05-14

The “Postgres Extension Family” release

This release lands ten extension integrations that turn pg_mentat into a Datalog hub for the Postgres ecosystem. Every integration is a SOFT dependency: nothing pg_mentat ships requires the upstream extension; each integration gates on a mentat.has_<ext>() detection helper. Where-fns generate SQL that calls the upstream extension’s operators directly; queries fail at execution (not compilation) when the extension isn’t loaded.

Added — Datalog where-fns

Search and ranking:

  • (rum-fulltext $ :attr "term") [[?e ?val ?score]] — BM25-style ranked fulltext via rum (PostgreSQL license; the permissive alternative to ParadeDB’s AGPL pg_search).
  • (similar-to $ :attr "needle" threshold) [[?e ?val ?score]] — trigram similarity via pg_trgm.
  • (levenshtein ?a ?b) ?d, (soundex ?s) ?c, (metaphone ?s ?n) ?c, (daitch-mokotoff ?s) ?c — phonetic and edit-distance functions via fuzzystrmatch.

Vector & semantic:

  • (vector-near $ :attr "[v1,v2,...]" k [:cosine|:l2|:inner]) [[?e ?dist]] — KNN via pgvector. Side-table aux pattern (mentat.attach_vector_attribute, set_vector, del_vector, create_hnsw_vector_index).
  • (infer-near $ :attr "text" k [:model]) [[?e ?dist]] — top-K KNN by model knowledge via pg_infer’s <~> operator.
  • (infer-similar a b) ?score, (infer-implies a b) ?bool — scalar pg_infer model functions.
  • (infer-walk "prompt" top) [[?layer ?feature ?score ?concept]], (infer-describe "entity") [[?relation ?target ?score ?layer]], (infer-predict "prompt" top) [[?token ?prob ?rank]] — set-returning pg_infer verbs.

Geospatial:

  • (geom-near $ :attr "WKT" k) [[?e ?dist]] — KNN by ST_Distance.
  • (geom-within $ :attr "WKT" radius) [[?e ?dist]] — within-distance via ST_DWithin.
  • (geom-contains $ :attr "WKT") [[?e]] — ST_Contains.
  • (geom-intersects $ :attr "WKT") [[?e]] — ST_Intersects. All via PostGIS, with side-table aux pattern (attach_geometry_attribute, set_geometry, del_geometry, create_gist_geometry_index) and automatic SRID detection from geometry_columns.

Added — SQL helpers (no Datalog surface)

Eleven new SQL-helper modules (pg_mentat/sql/12_*.sql through pg_mentat/sql/22_*.sql). Each declares a mentat.has_<ext>() detection function plus extension-specific helpers (index management, side-table attachment, etc.). The full helper inventory:

Extension Detection Headline helpers
pg_tre has_pg_tre (existing)
fuzzystrmatch has_fuzzystrmatch (where-fns only)
pg_trgm has_pg_trgm create_trgm_index, drop_trgm_index
rum has_rum create_rum_fulltext_index, drop_rum_fulltext_index
pgvector has_pgvector attach_vector_attribute, set_vector, del_vector, create_hnsw_vector_index
PgQue has_pgque pgque_emit_tx, pgque_disable_tx, pgque_register_consumer
pg_infer has_pg_infer create_infer_index, drop_infer_index
PostGIS has_postgis attach_geometry_attribute, set_geometry, del_geometry, create_gist_geometry_index, detach_geometry_attribute
PG19 SQL/PGQ has_pg19_graph create_vertex_view, create_edge_view, drop_*_view, create_property_graph_ddl
TimescaleDB has_timescaledb timescale_attach_transactions, timescale_attach_instant_datoms, timescale_set_transaction_retention
pg_partman has_pg_partman partman_attach_transactions, partman_set_transaction_retention, partman_run_maintenance
pg_cron has_pg_cron cron_schedule, cron_unschedule, cron_schedule_partman_maintenance, cron_schedule_vacuum_datoms

Added — transactional event stream

PgQue (NikolayS/PgQue) integration: mentat.pgque_emit_tx('queue') attaches a deferred constraint trigger to mentat.transactions that emits one mentat.tx-typed PgQue event per pg_mentat transaction. Event payload is JSON: tx, tx_instant, store_id, datom_count, plus a full datoms[] array with (e, a, v, vt, tx, added). PgQue is pure-PL/pgSQL — works on managed Postgres providers without shared_preload_libraries or restarts.

Added — documentation

Twelve new cookbook pages under docs/src/:

  • fuzzy-search.md (pg_tre — pre-existing, polished)
  • fuzzystrmatch.md, pg-trgm.md, rum.md, pgvector.md, postgres-fdw.md, pgque.md, pg_infer.md, postgis.md, pg19_graph.md, timescaledb.md, pg_partman.md, pg_cron.md

docs/INTEGRATIONS.md was the planning doc at the start of this work and is now maintained as the integration tracker, with everything in this release moved from the Tier 1 / Tier 2 / Tier 3 buckets to Done.

Fixed

  • FtsJoin entity-binding bug. The pre-existing FTS where-fns (fulltext, fuzzy-match) bound their entity variable into extra_var_bindings only, not var_to_alias. Subsequent EAV patterns referencing the same ?e failed to JOIN; cartesian products were silently masked by SELECT DISTINCT whenever the projected columns happened to collapse identically. Verified on a query that returned 9 rows when 3 were correct; the fix returns 3. All five FTS-style builders (fulltext, fuzzy-match, similar-to, rum-fulltext, vector-near, infer-near, geom-near, geom-within, geom-contains, geom-intersects) now propagate their entity binding into var_to_alias via a new FtsJoin.entity_alias field, populated before pattern processing.

Tests

Integration Tests Result
pg_tre (fuzzy-match) 7 7/7
fuzzystrmatch 7 7/7
pg_trgm 7 7/7
rum 6 6/6
pgvector 9 9/9
PgQue 5 5/5
pg_infer 10 10/10
PostGIS 10 10/10
Infra (pg19, ts, partman, cron) 13 13/13
Total integration tests 74 74/74

Smoke (scripts/smoke.sh): 11/11 PASS throughout.

Upgrading

pg_mentat--1.2.1--1.3.0.sql ships all eleven new helper-SQL modules as a single forward-only migration. The where-fn additions live in the loadable library and require no SQL upgrade.

ALTER EXTENSION pg_mentat UPDATE TO '1.3.0';

License notes

  • rum: PostgreSQL license. Use this instead of ParadeDB’s AGPL pg_search for BM25-style ranking in commercial deployments.
  • PostGIS: GPL-2.0+. Same as previous releases.
  • PgQue: Apache 2.0.
  • pg_infer: Apache 2.0. Experimental; PG18+; no managed-Postgres provider ships it yet.

[1.2.1] - 2026-05-13

Storage redesign + pg_tre integration. Wide-row mentat.datoms is now a VIEW over 9 narrow per-type tables with INSTEAD OF INSERT/DELETE triggers; store_id widened to BIGINT. pg_tre integration shipped with (fuzzy-match) where-fn for approximate-regex search.

See git log v1.2.0..v1.2.1 for full detail.

Earlier

For releases prior to 1.2.1, see git log and the migration scripts in crates/pg/pg_mentat/sql/.


Embedded mentat (0.x) — condensed history

The embedded SQLite store began at Mozilla as Project Mentat, a Datomic-like persistent relational store in Rust on SQLite. Selected releases:

  • 0.14.0 — last standalone mentat release before the merge; the mino scripting layer (mentat.store/*) and the shared EDN front-end used to build pg_mentat.
  • 0.11.1 (2018-08-09) — Android/Swift SDK updates; wording of several MentatError variants changed (ConflictingAttributeDefinitions, ExistingVocabularyTooNew, UnexpectedCoreSchema).
  • 0.11 (2018-07-31) — Mentat() constructor replaced by an open factory in the Android SDK.
  • 0.10 / 0.9 (2018-07) — early Rust releases; SQLite storage, Datalog query engine, pull expressions, schema evolution, and the transaction log.

The full pre-merge history is in the git log (git log -- crates/sqlite).