Contents
- Changelog
- [Unreleased]
- [1.10.1] — re-release of 1.10.0
- [1.10.0] — automatic indexes, faster count, stable at scale
- [1.9.1] — pg_dump now includes mentat’s data
- [1.9.0] — one SQL surface (edn_*) on SQLite, PostgreSQL and DuckDB
- [1.8.0] — 2026-09-26 — DuckDB backend + embedded feature parity
- [1.7.0] — 2026-09-26 — merged repository
- [1.6.2] - 2026-09-24
- [1.6.1] - 2026-09-08
- [1.6.0] - 2026-08-29
- [1.5.7] - 2026-07-07
- [1.5.6] - 2026-07-06
- [1.5.5] - 2026-07-06
- [1.5.4] - 2026-06-29
- [1.5.3] - 2026-06-29
- [1.5.2] - 2026-06-29
- [1.5.1] - 2026-06-18
- CI / maintenance release
- Changed
- Fixed (CI)
- Upgrading
- The “Append-Only Datom Log” release
- Changed — append-only datom log
- Added — current-state projection (the read path)
- Added — :db/noHistory attributes
- Fixed (exposed by the conversion)
- Fixed — query/transaction correctness (fail-loud)
- Tests
- Upgrading
- [1.4.0] - 2026-06-16
- [1.3.0] - 2026-05-14
- [1.2.1] - 2026-05-13
- Earlier
- Embedded mentat (0.x) — condensed history
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; inadaptivemode, range-filtered history attributes get partial indexes, tracked inmentat.managed_indexesand dropped aftermentat.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/uniqueattributes and:db/indexrefs get a usable value index in the default mode (the schema’s old partial AVET indexes never matched mentat’s SQL).AutoIndex::Adaptiveadds 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 thementat_managed_indexesregistry are ever dropped.
- PostgreSQL: every
edn_q_rows(query, inputs)(PostgreSQL): streams one JSONB array per row, for results too large foredn_q’s single JSONB value.- DuckDB as a server:
crates/duckdb/server/serve.shruns mentat inside a long-lived DuckDB Quack server (quack_serve, token auth, localhost by default), so clients without mentat sendedn_q/edn_toverquack_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:
.qtakes the same options JSON as SQLedn_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:ininputs, and the shared Datomic model suite now covers inputs, history patterns,casandretractEntityon 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,avgandcountof a value variable aggregate the set of bindings.(sum ?heads)over heads 1, 1, 1, 3 is now 4 (was 6); add:with ?efor the old result.:withis now honoured (it was ignored),count-distinctworks (it was rejected), andsum/avgover 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 largeGROUP BYs on one connection slower every time.MENTAT_TEMP_STORE=2restores 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 setsmmap_size(MENTAT_MMAP_SIZE, default 1 GiB), which helps downstream builds that don’t use this repo’s config. - The
mentat.max_result_rowsandmentat.temp_file_limiterrors 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::openscanned the whole transaction log (16 s at 10M datoms, now 50 ms); concurrent readers ran slower than one. - PostgreSQL:
get-elsealways 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:ininput for a ref attribute’s value now matches the entity (it was compared as a long and matched nothing); and a declared:inbinding with no input value is an error instead of being silently ignored.mentatd’s:qpath 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/retractEntitydoesn’t retract references to the entity (Datomic does), andcas/retractEntitydon’t take lookup refs, on either backend.- The embedded scripting
qstill refuses an as-of/since db. - PostgreSQL
mentat_explain/mentat_query_sqlignore collection:ininputs, and a 5-place history pattern insidenotor a rule body still reads current state. edn_q_rowsbuilds 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_dumpof a database using pg_mentat restored every store EMPTY. All mentat tables are extension members, andpg_dumpdumps the data of an extension member only when the table is registered withpg_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 nightlypg_dumpheld 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 tablesCREATE EXTENSIONseeds are registered with a filter that excludes the seed rows, sopg_restoreinto 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_pullandedn_eval(mino scripting) on every backend. - PostgreSQL:
mentat_transact,mentat_query,mentat_pullandmentat_evalstill 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 thescriptfeature). Thementat.q/mentat.t/mentat.pullaliases now call the new names, andmentat_query’sinputsargument now defaults to'{}'. - DuckDB (breaking):
mentat_transactandmentat_queryare gone, replaced byedn_tandedn_qwith no aliases (the extension hadn’t been published yet). The placeholdermentat_hello()is removed. - DuckDB (breaking):
edn_qnow returns strings as plain text (Alice). Before, it returned them with the quotes included ("Alice"), so joins against nativeVARCHARcolumns 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 (.loadin thesqlite3CLI,load_extensionin Python, and so on).edn_qreturns pg_mentat’s JSON shape, sojson_each(edn_q(...))joins against native tables. The functions areSQLITE_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) andedn_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:informs 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 newQueryInputs::merge. mentat::script::Interpreter::with_default_path(path): a sandboxed interpreter where(mentat.store/open)with no argument openspath.
Fixed
- PostgreSQL: reactive subscription triggers called
mentat_querywith one argument, which no function accepted, so every subscription failed when its trigger fired. They now calledn_q(query, '{}'). - PostgreSQL:
mentat_query_statsandmentat_slow_queriesnow count calls to the newedn_*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) andmentat_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. Seedocs/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 theConnequivalents) evaluate a query against the database as of, or since, a transaction. - Non-scalar
:inbindings — collection[?x ...], tuple[?a ?b], and relation[[?a ?b]]inputs (previously accepted by the parser but dropped).
- History patterns
mentat-scriptshared crate (crates/script) — the minomentat.store/*scripting layer, previously duplicated in the SQLite and PostgreSQL backends, is now one crate behind aScriptBackendtrait, exercised by a single model test suite that runs against both backends (and an in-memory fake). The sandboxed, resource-limitedmentat_evalon the PostgreSQL side is unchanged.
Changed
- The scripting
transacttx-report now carries:mentat.store/db-afteron 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/casand:db/retractEntity(both:db.fn/*and:db/*spellings).casreads the current value inside the write transaction and aborts the whole transaction with a typedCasMismatcherror on mismatch;retractEntityretracts every datom with the entity as subject and recurses through:db/isComponentchildren. 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-ofq, non-scalar:inbindings — planned for the embedded side in a later release). - mino refreshed to upstream’s final release (
9c65bb50), now maintained in this repository:#uuidreads to a real UUID value and round-trips,#instprints as a reader literal,read-string, classed/keywordcatch, regex lookahead, a distinctdelaytype, a pluggable store backend seam, and newcore.cljhelpers. 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/recurand 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/valueTypeis 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 speculativedb -> db'that runs the full pipeline in a savepoint and rolls back (does not commit).q/q-once— arbitrary Datalog. Because pg_mentat’smentat_queryacceptsasOf/sinceinputs, fullqruns 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 wholeALTER EXTENSIONwhen the sequence’slast_valuealready exceeds that ceiling (RESTART value (N) cannot be greater than MAXVALUE (m)). 1.5.7 ships a direct1.5.5→1.5.7upgrade edge (PostgreSQL picks the shortest path, bypassing the broken 1.5.6 step) and a1.5.6→1.5.7edge. Both bound each sequence toGREATEST(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. ANOTICEflags 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 — rewritinge,a, and incoming refvacross the nine log tables + nine current projection tables, and the schema/idents catalogs — while the transaction keeps its id (its id anchors thetxcolumn and basis-t /:as-ofmonotonicity, so it must not move). Verify withentid_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 at1000001, leaving1000000as 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, whichrepair_entid_collisionsclears.)
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 onlySTART WITHand noMAXVALUE, 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 permentat.t, so a write-heavy store (e.g. ~33k tx/day) exhausted it in weeks and thenmentat.tbegan issuing ids outside its band.Fresh installs use a new, disjoint, generous layout, and every partition sequence is bounded to its band with
MINVALUE/MAXVALUEso 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_testsfailures (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 insidementat.t, on every value type. Onlydatoms_ref_newanddatoms_keyword_newshipped 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 byv. 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 insidementat.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_introspectionregression 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_returninlookup_by_ident(the 1.5.3 read-only-SPI conversion left a redundantlet result = ...; result). The 1.5.3 tag’sclippy -D warningsCI 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_queryon the primary. 1.5.2 routed the query path through read-only SPI, which flips the transaction read-only beforeapply_optimizer_hintsruns; itsSET LOCALresource 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 viapg_sys::set_config_optionwithGUC_ACTION_LOCALinstead of a SQLSET, 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/:sincetime-travel queries,mentat.lookup_by_ident, and thehas_<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(was1.88.0). pgrx 0.17 usesNonNull::from_mut, stable since 1.89, so 1.88 failed to compile pgrx. cargo fmt --allapplied across the workspace (the CI format check had drifted on ~115 files).tokio-postgres→ 0.7.18 andpostgres-protocol→ 0.6.12 in thementatdclient, closing RUSTSEC-2026-0178/0179/0180 (DoS). These are not in the PostgreSQL extension’s runtime.- Added per-crate
license = "Apache-2.0"tomentat_coreandcore_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 thegeom-withinradius check). - GitHub
docsworkflow: version-pinned the mdBook download URL; enabled GitHub Pages on the mirror. - Container workflow: build
Dockerfile(not the nonexistentContainerfile), added thedemo.sqlthe image references, glob the versioned base SQL, and test by querying the running image (the lean runtime image has no Rust toolchain). cargo pgrx testjobs (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--ignorefor the four advisories with no clean fix (transitive via pgrx / prometheus). - Optional-extension test suites (pgvector, rum, fuzzystrmatch, pg_trgm):
route the speculative
CREATE EXTENSIONthrough a subtransaction helper so a missing third-party extension skips cleanly instead of poisoning the test transaction. - Nix flake:
export -fthe dev-shell helpers; writableCARGO_HOME/PGDATA; providepg_configviapostgresql.pg_config; add readline/zlib/icu.devoutputs 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. fillfactor85/90 → 100 on the ninedatoms_*_newlog 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_datomsview (union over the nine, legacydatoms-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;0means consistent. Used as the cutover safety gate and in tests.
Added — :db/noHistory attributes
- Datomic-compatible
:db/noHistory trueattribute 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-ofsees only the current value (the trade for zero bloat).
Fixed (exposed by the conversion)
:db.fn/casread the current value viadatoms WHERE added=true, which in the append-only model returns superseded historical assertions too. CAS now readsmentat.current_datoms.batch_insert_datomsdedups by full PK(e,a,v,tx,added): CAS queues a retraction and the cardinality-one replace path independently queues the same one; a singleINSERT ... ON CONFLICTcannot list a key twice.is_duplicate_cardinality_manyreads the projection (presence == live) rather than anadded=truelog 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 onmentat.current_textrather 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:findclause is now implemented; it previously produced a NULL column. The result nests as a JSON object.- The
mentat.edntype’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
:inand 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
:findvariable, 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.0in-place upgrade is qualified end to end: install 1.4.0, load data,ALTER EXTENSION … UPDATE TO '1.5.0', thenverify_current_projection(0) = 0and 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-wayUNION ALLprobe 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 addedcovering 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 pert().
Operations
- Autovacuum retune on all narrow tables +
transactions. The previousautovacuum_vacuum_scale_factor = 0.05(and the PG default 0.2) effectively stops triggering on large tables, so they bloat without bound — most visiblydatoms_instant_new, where monotonic attributes (:first-seen/:last-seen/:observed-at) are re-asserted every sync. All ninedatoms_*_newtables andmentat.transactionsnow ship withscale_factor = 0+ a fixedthreshold = 50000, so autovacuum fires on a constant dead-tuple count regardless of table size. Applied byCREATE EXTENSIONand 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 reada = mentat.attr_id(':person/name')instead ofa = 1308861.mentat.current(e, a)andmentat.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-queryDISTINCT ON/LATERALfan-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 withattribute_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_datomfunction (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’saddedflag 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_idresolution,current()latest-value + NULL-absent + post-replace, the cardinality-one fast-path correctness (exactly one live datom after repeated replaces), idempotent-reassert no-churn, andattribute_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 AGPLpg_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 byST_Distance.(geom-within $ :attr "WKT" radius) [[?e ?dist]]— within-distance viaST_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 fromgeometry_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 intoextra_var_bindingsonly, notvar_to_alias. Subsequent EAV patterns referencing the same?efailed to JOIN; cartesian products were silently masked bySELECT DISTINCTwhenever 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 intovar_to_aliasvia a newFtsJoin.entity_aliasfield, 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_searchfor 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
mentatrelease before the merge; theminoscripting layer (mentat.store/*) and the shared EDN front-end used to buildpg_mentat. - 0.11.1 (2018-08-09) — Android/Swift SDK updates; wording of several
MentatErrorvariants changed (ConflictingAttributeDefinitions,ExistingVocabularyTooNew,UnexpectedCoreSchema). - 0.11 (2018-07-31) —
Mentat()constructor replaced by anopenfactory 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).