Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wQUun-001b74-2n for pgsql-announce@arkaria.postgresql.org; Fri, 22 May 2026 18:48:10 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wQUul-00E653-2k for pgsql-announce@arkaria.postgresql.org; Fri, 22 May 2026 18:48:08 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wQUul-00E64D-0C for pgsql-announce@lists.postgresql.org; Fri, 22 May 2026 18:48:08 +0000 Received: from mahout.postgresql.org ([2001:4800:3e1:1::227]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1wQUuj-00000000vos-2GUB for pgsql-announce@lists.postgresql.org; Fri, 22 May 2026 18:48:07 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=postgresql.org; s=20171124; h=Message-ID:Date:Reply-To:From:To:Subject: MIME-Version:Content-Type:Sender:Cc:Content-Transfer-Encoding:Content-ID: Content-Description:In-Reply-To:References; bh=plPSAAJ9ZJMC5Wh4yMHNLBND0bEQB8v9cPRhCsLDMjk=; b=eZ80TETBTGTItRUqhXOKB/HJYz AH3vMGDFXeE1T22ldtGcFA/uf1UT5TMj6KYxbv1Z599zF1rrBmMGNeoAXB7xZFsS+O5/2SWoC9rcU 6XZ84raVc5TFtctQPmlzzTog3G5r+0MA5gP9MmFmvZ4AxEZkkx2L84IowkV4EzSLFpUs9DUXrWDvE oOaTxCbraQsRVo38dpuAQ54bgQBKaRv7iSJVMt1mkDW/IpCp/OGX0/RRxPKiWmWIcwnz+TDxJe8hv Vm8t1XWoOLiBgjjSlOWWsqcyAhPXrf3fanSEY3sXDxp+GxUYJsc+06ccxta6jhHMv+eC4bVnKY/gD lQKhPcSA==; Received: from wrigleys.postgresql.org ([2a02:16a8:dc51::60]) by mahout.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wQUui-000uYH-05 for pgsql-announce@lists.postgresql.org; Fri, 22 May 2026 18:48:04 +0000 Received: from localhost ([127.0.0.1] helo=wrigleys.postgresql.org) by wrigleys.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wQUug-005Afd-36 for pgsql-announce@lists.postgresql.org; Fri, 22 May 2026 18:48:02 +0000 Content-Type: multipart/alternative; boundary="===============0919753575674034753==" MIME-Version: 1.0 Subject: pg_mentat 1.3.0 released -- Datomic-compatible Datalog inside PostgreSQL To: PostgreSQL Announce From: Greg Burd via PostgreSQL Announce Reply-To: greg@burd.me Date: Fri, 22 May 2026 18:47:40 +0000 Message-ID: <177947566062.799.8820207735562752444@wrigleys.postgresql.org> X-Auto-Response-Suppress: All Auto-Submitted: auto-generated X-pglister-tags: related X-pglister-tagsig: 7850d1454fbb3381f1a5885ee87a34355e048185b95b84e5ddf99ba15f8e19e8 List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --===============0919753575674034753== Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable I am pleased to announce the first public release of [pg_mentat] (https://g= ithub.com/gburd/pg_mentat), a PostgreSQL extension that implements Datomic's data model -- immutable facts (datoms), schema-first a= ttributes, a full Datalog query compiler, the pull API, time travel, and AC= ID transactions -- entirely inside PostgreSQL. pg_mentat is built with pgrx 0.17 in Rust and supports PostgreSQL 13 throug= h 18. The current release is 1.3.0, the "Postgres Extension Family" release= , which adds Datalog where-fns that bridge into rum, pg_trgm, fuzzystrmatch= , pgvector, pg_infer, PostGIS, and several other extensions as soft depende= ncies (nothing pg_mentat ships requires any of them, but where-fns light up= automatically when they are present). An optional companion daemon, mentatd, speaks the Datomic client wire proto= col (EDN, Transit+JSON, Transit+MsgPack) over HTTP for applications that already expect it. Highlights ---------- * Full Datalog query language: relation / collection / tuple / scalar find specifications; pattern matching, predicates, function expressions; not / not-join / or / or-join; named rules and recursive rules with cycle detection. * Pull API with wildcards, reverse refs, nested / recursive pulls, defaults, rename, and limit clauses -- arbitrarily shaped JSON results in a single round trip. * Time travel: as-of, since, history, tx-range. Every datom is timestamped by transaction; the database is an append- only log of immutable facts, and any past state is queryable. * GDPR-compliant excision: permanent deletion of datoms by entity, attribute, or value, with the audit trail removed. * Reactive subscriptions via PostgreSQL LISTEN / NOTIFY. * Storage: nine narrow per-type datom tables (ref, long, string, bool, double, inst, kw, uuid, bytes) with the four canonical Datomic indexes (EAVT, AEVT, AVET, VAET) covered by ordinary PostgreSQL btrees. * Soft integrations new in 1.3.0: rum-fulltext, similar-to (pg_trgm), levenshtein / soundex / metaphone / daitch- mokotoff (fuzzystrmatch), vector-near (pgvector), infer-near / infer-similar / infer-implies / infer-walk / infer- describe / infer-predict (pg_infer), geom-near / geom- within / geom-contains / geom-intersects (PostGIS), and detection helpers (mentat.has_()) for capability discovery. Why have EDN and Datalog next to relational tables? --------------------------------------------------- Datalog and SQL are not redundant; they answer different questions well. Pu= tting them in the same database, sharing the same MVCC, the same WAL, the s= ame backup, and the same connection pool unlocks workloads that are awkward= in either alone. * Knowledge graphs that join your business data. Encode an ontology -- categories, hierarchies, taxonomies, typed relationships -- as datoms with refs, then query with Datalog rules. Recursive rules express transitive closure in a few lines (no recursive CTE, no cycle-detection boilerplate). Then expose that graph as a PostgreSQL VIEW via mentat.mentat_query_view(...) and JOIN it directly with your relational tables in ordinary SQL. One database, one transaction, two query languages. * Schema as data -- queryable like everything else. Schema attributes (:db/ident, :db/valueType, :db/cardinality, :db/unique, :db/index, :db/fulltext, :db/isComponent, :db/noHistory) live in the datom store. You query the schema with the same Datalog you use for application data, instead of grovelling in pg_catalog or information_schema. * Audit and time travel without triggers. Because datoms are immutable, "what did this entity look like at tx 1000005?" and "what changed between tx 1000003 and 1000007?" are first-class queries. No audit tables, no triggers, no temporal-extension contortions. Compliance workloads that take weeks to retrofit on a relational schema are a one-line query in pg_mentat. * Speculative transactions. mentat.mentat_with('[ ... ]') runs a transaction in memory, returns the full report (tempid resolution, new datoms, constraint outcomes), and writes nothing. Validate imports, preview merges, or detect conflicts before committing. * EDN as the data interchange format. Schema, transactions, and queries are all EDN. Tagged values (#inst, #uuid, keywords, sets, vectors) survive a round trip without ad hoc JSON conventions. Existing Clojure / Datomic code, including pull patterns and Datalog expressions, runs against pg_mentat with minor adjustments. A short example: a graph traversal that would be 20+ lines of recursive CTE= in plain SQL: ``` SELECT mentat.q(' [:find ?name :in $ ?start :where [?start :person/name ?start-name] (reachable ?start ?friend) [?friend :person/name ?name] :rules [[(reachable ?a ?b) [?a :person/friends ?b]] [(reachable ?a ?b) [?a :person/friends ?c] (reachable ?c ?b)]]] ', '["Alice"]'); ``` Datalog says what to find. The compiler decides how. The plan is ordinary S= QL, executed by ordinary PostgreSQL, against ordinary PostgreSQL pages. How pg_mentat fits among the alternatives ----------------------------------------- * Datomic / XTDB / Crux: hosted Datalog stores, often with excellent query languages and weaker operational stories for shops that have already standardized on PostgreSQL. pg_mentat brings their data model into the database you already run, back up, monitor, and replicate. * pg_graphql / Apache AGE: graph layers over PostgreSQL with Cypher / GraphQL respectively. Different query languages, different data models, no time-travel semantics, no pull API, no built-in immutable-facts model. * hstore / jsonb: schema-flexible storage without a query language designed for joins across attributes. pg_mentat gives you Datalog, refs, recursive rules, and the pull API. A note for the Datomic / Mentat / Datalog community --------------------------------------------------- If you have been writing Clojure against Datomic, Datascript, or the origin= al Mentat (RIP), pg_mentat will feel familiar. EDN schema, transactions, a= nd queries; the pull API; as-of / since / history; uniqueness via :db.uniqu= e/identity (which makes upserts trivial -- no INSERT ON CONFLICT, no MERGE)= -- the surface is intentionally close. The differences are deliberate and = pragmatic: storage is PostgreSQL, transactions are PG transactions, the ful= ltext attribute uses tsvector, and operational tooling is whatever you alre= ady use for PostgreSQL (pg_basebackup, pg_dump, pgBackRest, logical replica= tion, WAL-G, and so on). I would love feedback from anyone who has producti= on experience with Datomic-style stores and wants a Datalog-on-Postgres pat= h -- specifically: which Datalog forms do you rely on that pg_mentat does n= ot yet implement, and which Datomic-isms do you NOT want carried over? File= issues or send mail. Quick start ----------- ``` CREATE EXTENSION pg_mentat; SELECT mentat.t('[ {:db/ident :person/name :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/unique :db.unique/identity} {:db/ident :person/age :db/valueType :db.type/long :db/cardinality :db.cardinality/one} ]'); SELECT mentat.t('[{:person/name "Alice" :person/age 30}]'); SELECT mentat.q(' [:find ?name ?age :where [?e :person/name ?name] [?e :person/age ?age] [(> ?age 18)]]'); ``` A note on stability and feedback -------------------------------- pg_mentat is new software. The on-disk schema, the function surface, the ED= N encoding, and the Datalog dialect are documented and covered by tests, an= d 1.3.0 ships ten extension integrations gated behind capability detection = -- but the project is young, and bugs you can reach are very likely bugs no= body has reached yet. It is not a beta and not a research toy; it is real s= oftware released early, and it should be used with appropriate caution. Bug reports, feature requests, and pull requests are very welcome -- partic= ularly from people who already speak Datalog and can spot semantic gaps qui= ckly. Links ----- * Repository: [https://github.com/gburd/pg_mentat](https://github.com/gb= urd/pg_mentat) * Issues: [https://github.com/gburd/pg_mentat/issues](https://github= .com/gburd/pg_mentat/issues) * Integrations: [https://github.com/gburd/pg_mentat/blob/main/docs/INTEGRA= TIONS.md](https://github.com/gburd/pg_mentat/blob/main/docs/INTEGRATIONS.md) --===============0919753575674034753== Content-Type: text/html; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable pg_mentat 1.3.0 released -- Datomic-compatible Datalog inside Po= stgreSQL
 

pg_mentat 1.3.0 released -- Datomic-compatible Datalog inside Postgre= SQL

I am pleased to announce the first public r= elease of [pg_mentat] (https://github.com/gburd/pg_mentat), a PostgreSQL ex= tension that implements Datomic's data model -- immutable facts (datoms), schema-first a= ttributes, a full Datalog query compiler, the pull API, time travel, and AC= ID transactions -- entirely inside PostgreSQL.

pg_mentat is built with pgrx 0.17 in Rust a= nd supports PostgreSQL 13 through 18. The current release is 1.3.0, the "Po= stgres Extension Family" release, which adds Datalog where-fns that bridge = into rum, pg_trgm, fuzzystrmatch, pgvector, pg_infer, PostGIS, and several = other extensions as soft dependencies (nothing pg_mentat ships requires any= of them, but where-fns light up automatically when they are present).

An optional companion daemon, mentatd, spea= ks the Datomic client wire protocol (EDN, Transit+JSON, Transit+MsgPack) ov= er HTTP for applications that already expect it.

Highlights

  • Full Datalog query language: relation / col= lection / tuple / scalar find specifications; pattern matching, predicates, function expressions; not / not-join / or / or-join; named rules and recursive rules with cycle detection.

  • Pull API with wildcards, reverse refs, nest= ed / recursive pulls, defaults, rename, and limit clauses -- arbitrarily shaped JSON results in a single round trip.

  • Time travel: as-of, since, history, tx-rang= e. Every datom is timestamped by transaction; the database is an append- only log of immutable facts, and any past state is queryable.

  • GDPR-compliant excision: permanent deletion= of datoms by entity, attribute, or value, with the audit trail removed.

  • Reactive subscriptions via PostgreSQL LISTE= N / NOTIFY.

  • Storage: nine narrow per-type datom tables = (ref, long, string, bool, double, inst, kw, uuid, bytes) with the four canonical Datomic indexes (EAVT, AEVT, AVET, VAET) covered by ordinary PostgreSQL btrees.

  • Soft integrations new in 1.3.0: rum-fulltex= t, similar-to (pg_trgm), levenshtein / soundex / metaphone / daitch- mokotoff (fuzzystrmatch), vector-near (pgvector), infer-near / infer-similar / infer-implies / infer-walk / infer- describe / infer-predict (pg_infer), geom-near / geom- within / geom-contains / geom-intersects (PostGIS), and detection helpers (mentat.has_<ext>()) for capability discovery.

Why have EDN and Datalog next t= o relational tables?

Datalog and SQL are not redundant; they ans= wer different questions well. Putting them in the same database, sharing th= e same MVCC, the same WAL, the same backup, and the same connection pool un= locks workloads that are awkward in either alone.

  • Knowledge graphs that join your business da= ta. Encode an ontology -- categories, hierarchies, taxonomies, typed relationships -- as datoms with refs, then query with Datalog rules. Recursive rules express transitive closure in a few lines (no recursive CTE, no cycle-detection boilerplate). Then expose that graph as a PostgreSQL VIEW via mentat.mentat_query_view(...) and JOIN it directly with your relational tables in ordinary SQL. One database, one transaction, two query languages.

  • Schema as data -- queryable like everything= else. Schema attributes (:db/ident, :db/valueType, :db/cardinality, :db/unique, :db/index, :db/fulltext, :db/isComponent, :db/noHistory) live in the datom store. You query the schema with the same Datalog you use for application data, instead of grovelling in pg_catalog or information_schema.

  • Audit and time travel without triggers. Because datoms are immutable, "what did this entity look like at tx 1000005?" and "what changed between tx 1000003 and 1000007?" are first-class queries. No audit tables, no triggers, no temporal-extension contortions. Compliance workloads that take weeks to retrofit on a relational schema are a one-line query in pg_mentat.

  • Speculative transactions. mentat.mentat_with('[ ... ]') runs a transaction in memory, returns the full report (tempid resolution, new datoms, constraint outcomes), and writes nothing. Validate imports, preview merges, or detect conflicts before committing.

  • EDN as the data interchange format. Schema, transactions, and queries are all EDN. Tagged values (#inst, #uuid, keywords, sets, vectors) survive a round trip without ad hoc JSON conventions. Existing Clojure / Datomic code, including pull patterns and Datalog expressions, runs against pg_mentat with minor adjustments.

A short example: a graph traversal that wou= ld be 20+ lines of recursive CTE in plain SQL:

SELECT mentat.q(' [:find ?name :in $ ?start :where [?start :person/name ?start-name] (reachable ?start ?friend) [?friend :person/name ?name] :rules [[(reachable ?a ?b) [?a :person/friends ?b]] [(reachable ?a ?b) [?a :person/friends ?c] (reachable ?c ?b)]]] ', '["Alice"]');

Datalog says what to find. The compiler dec= ides how. The plan is ordinary SQL, executed by ordinary PostgreSQL, agains= t ordinary PostgreSQL pages.

How pg_mentat fits among the al= ternatives

  • Datomic / XTDB / Crux: hosted Datalog stor= es, often with excellent query languages and weaker operational stories for shops that have already standardized on PostgreSQL. pg_mentat brings their data model into the database you already run, back up, monitor, and replicate.

  • pg_graphql / Apache AGE: graph layers over= PostgreSQL with Cypher / GraphQL respectively. Different query languages, different data models, no time-travel semantics, no pull API, no built-in immutable-facts model.

  • hstore / jsonb: schema-flexible storage wi= thout a query language designed for joins across attributes. pg_mentat gives you Datalog, refs, recursive rules, and the pull API.

A note for the Datomic / Mentat= / Datalog community

If you have been writing Clojure against Da= tomic, Datascript, or the original Mentat (RIP), pg_mentat will feel famili= ar. EDN schema, transactions, and queries; the pull API; as-of / since / h= istory; uniqueness via :db.unique/identity (which makes upserts trivial -- = no INSERT ON CONFLICT, no MERGE) -- the surface is intentionally close. The= differences are deliberate and pragmatic: storage is PostgreSQL, transacti= ons are PG transactions, the fulltext attribute uses tsvector, and operatio= nal tooling is whatever you already use for PostgreSQL (pg_basebackup, pg_d= ump, pgBackRest, logical replication, WAL-G, and so on). I would love feedb= ack from anyone who has production experience with Datomic-style stores and= wants a Datalog-on-Postgres path -- specifically: which Datalog forms do y= ou rely on that pg_mentat does not yet implement, and which Datomic-isms do= you NOT want carried over? File issues or send mail.

Quick start

``` CREATE EXTENSION pg_mentat;

SELECT mentat.t('[
  {:db/ident :person/name :db/valueType :db.type/string
   :db/cardinality :db.cardinality/one
   :db/unique :db.unique/identity}
  {:db/ident :person/age  :db/valueType :db.type/long
   :db/cardinality :db.cardinality/one}
]');

SELECT mentat.t('[{:person/name "Alice" :person/age 30}]');

SELECT mentat.q('
  [:find ?name ?age
   :where [?e :person/name ?name]
          [?e :person/age ?age]
          [(> ?age 18)]]');

```

A note on stability and feedbac= k

pg_mentat is new software. The on-disk sche= ma, the function surface, the EDN encoding, and the Datalog dialect are doc= umented and covered by tests, and 1.3.0 ships ten extension integrations ga= ted behind capability detection -- but the project is young, and bugs you c= an reach are very likely bugs nobody has reached yet. It is not a beta and = not a research toy; it is real software released early, and it should be us= ed with appropriate caution.

Bug reports, feature requests, and pull req= uests are very welcome -- particularly from people who already speak Datalo= g and can spot semantic gaps quickly.

Links

  • Repository: <= a href=3D"https://github.com/gburd/pg_mentat" style=3D"color: #3498db; text= -decoration: underline">https://github.com/gburd/pg_mentat
  • Issues: <= a href=3D"https://github.com/gburd/pg_mentat/issues" style=3D"color: #3498d= b; text-decoration: underline">https://github.com/gburd/pg_mentat/issues
  • Integrations: <= a href=3D"https://github.com/gburd/pg_mentat/blob/main/docs/INTEGRATIONS.md= " style=3D"color: #3498db; text-decoration: underline">https://github.com/g= burd/pg_mentat/blob/main/docs/INTEGRATIONS.md
This email was sent to you from Greg Burd. It was delivered on their behalf= by the PostgreSQL project. Any questions about the content of the message shou= ld be sent to Greg Burd.

You were sent this email as a subscriber of the pgsql-announce mai= linglist, for the content tag Related Open Source. To unsubscribe from further emails, or change which emails you want to receive, please click th= e personal unsubscribe link that you can find in the headers of this email, or visit https://lists.postgresql.org/unsubscribe/.
 
--===============0919753575674034753==--