Contents
pg_mentat
PostgreSQL extension providing a Datomic-compatible Datalog query engine with a native EDN data type.
Overview
pg_mentat is a PostgreSQL extension built with pgrx
that provides:
- Datalog Query Engine – execute Datalog queries with find specs, rules, aggregates, predicates, OR/NOT clauses, and full-text search
- Transaction Processing – assert and retract facts using EDN transaction format with tempid resolution, lookup refs, and upsert semantics
- Pull API – retrieve entity attributes with pattern matching, nested traversal, reverse lookups, limits, defaults, and recursion
- Temporal Database – time-travel queries with as-of, since, and full history modes
- Native EDN Type – first-class PostgreSQL type for Extensible Data Notation with collection access, type predicates, and equality operators
- SQL Integration – all operations exposed as SQL functions, composable with CTEs, window functions, JOINs, and any PostgreSQL feature
- EDN Helper Functions – batch operations, import/export, entity helpers
Quick Start
Prerequisites
- PostgreSQL 13-19 (16 recommended; 19 tested on 19rc1)
- Rust stable toolchain (1.88+)
- LLVM/Clang development libraries
- cargo-pgrx 0.19.3 (Rust 1.96+)
Build and Install
cd pg_mentat
cargo pgrx install --release
Interactive Session
cargo pgrx run pg16
CREATE EXTENSION pg_mentat;
-- Define schema
SELECT edn_t('[
{:db/ident :person/name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
{:db/ident :person/email
: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}
]');
-- Transact data
SELECT edn_t('[
{:db/id "alice" :person/name "Alice" :person/email "alice@example.com" :person/age 30}
{:db/id "bob" :person/name "Bob" :person/email "bob@example.com" :person/age 25}
]');
-- Query
SELECT edn_q('
[:find ?name ?email
:where [?e :person/name ?name] [?e :person/email ?email]]
', '{}');
-- Pull
SELECT edn_pull('[*]', 10000);
-- Entity
SELECT mentat_entity(10000);
SQL Function Reference
Core API
| Function | Description |
|---|---|
edn_t(edn TEXT) |
Process EDN transactions (assert, retract, retractEntity) |
edn_q(query TEXT, inputs JSONB) |
Execute Datalog queries with temporal and pagination options |
edn_pull(pattern TEXT, entity_id BIGINT) |
Pull entity attributes by pattern |
mentat_pull_many(pattern TEXT, entity_ids BIGINT[]) |
Pull attributes for multiple entities |
mentat_entity(entity_id BIGINT) |
Get all attributes of an entity as JSONB |
mentat_schema() |
Return current schema as JSONB |
mentat_explain(query TEXT, inputs JSONB) |
Show query execution plan and generated SQL |
Since 1.9.0 the core functions use the edn_* names shared with the SQLite and
DuckDB extensions. The old names mentat_transact, mentat_query,
mentat_pull and mentat_eval remain as deprecated SQL wrappers.
Scripting API (optional script feature)
Built with --features script, pg_mentat embeds the pure-Rust
mino interpreter (a Clojure dialect)
and exposes a mentat.store/* namespace — a Datomic-in-Clojure surface where a
db is an immutable database value, not a connection.
| Function | Description |
|---|---|
edn_eval(script TEXT) |
Evaluate a mino script; returns its result as EDN (pr-str) text |
-- Transact a schema, then query. `db` is an immutable value carrying its basis.
SELECT edn_eval($$
(mentat.store/transact (mentat.store/open)
[{:db/ident :person/name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}])
$$);
SELECT edn_eval($$
(mentat.store/q (mentat.store/db (mentat.store/open))
'[:find ?n :where [?e :person/name ?n]])
$$);
The mentat.store/* primitives: open (conn handle), db (immutable db value
at the current basis), transact (commits), with (speculative db -> db',
no commit), q/q-once, pull, entity, read, datoms, and as-of /
since (temporal db values). Because edn_q accepts asOf/since
inputs, full Datalog q runs against a historical basis — an advantage over
the standalone Mentat crate. Default off; a build without the feature pulls and
costs nothing.
EDN Helper Functions (mentat schema)
| Function | Description |
|---|---|
mentat.batch(edn TEXT) |
Execute multiple operations in a single batch |
mentat.export_edn(entity_ids BIGINT[]) |
Export entities to EDN format |
mentat.import_edn(edn TEXT) |
Import entities from EDN format |
mentat.query_export_edn(query TEXT, inputs JSONB) |
Query and export matching entities |
mentat.export_all_edn() |
Export entire database to EDN |
Entity Helper Functions (mentat schema)
| Function | Description |
|---|---|
mentat.lookup_by_ident(attr TEXT, value TEXT) |
Look up entity by attribute value |
mentat.entity_attrs(entity_id BIGINT) |
List attribute idents for an entity |
mentat.attribute_values(attr TEXT) |
Get all values for an attribute |
mentat.retract_entity(entity_id BIGINT) |
Retract all facts about an entity |
Operational Functions
| Function | Description |
|---|---|
mentat_query_stats() |
Query performance and database statistics |
mentat_storage_stats() |
Table and index size information |
mentat_slow_queries(threshold_ms) |
Find slow functions and heavy transactions |
mentat_stmt_cache_stats() |
Prepared statement cache statistics |
mentat_stmt_cache_clear() |
Clear prepared statement cache |
EDN Type Functions
| Function | Description |
|---|---|
edn_get(map, key) |
Get value from map by key |
edn_nth(vector, index) |
Get element by 0-based index |
edn_count(collection) |
Collection size |
edn_contains(collection, element) |
Membership test |
edn_keys(map) |
Extract map keys |
edn_values(map) |
Extract map values |
edn_is_nil, edn_is_boolean, edn_is_integer, edn_is_float, edn_is_text, edn_is_keyword, edn_is_vector, edn_is_list, edn_is_set, edn_is_map |
Type predicates |
SQL Integration
pg_mentat functions return standard PostgreSQL types (JSONB, TEXT, BIGINT), making them composable with all PostgreSQL features:
-- CTEs with Datalog
WITH engineers AS (
SELECT elem->>0 AS eid, elem->>1 AS name
FROM edn_q('[:find ?e ?name :where
[?e :person/department ?d] [?d :dept/name "Engineering"]
[?e :person/name ?name]]', '{}') AS q,
jsonb_array_elements(q->'results') AS elem
)
SELECT * FROM engineers;
-- Window functions
WITH salaries AS (
SELECT (elem->>1)::text AS name, (elem->>2)::int AS salary
FROM edn_q('[:find ?e ?name ?salary :where
[?e :person/name ?name] [?e :person/salary ?salary]]', '{}') AS q,
jsonb_array_elements(q->'results') AS elem
)
SELECT name, salary, RANK() OVER (ORDER BY salary DESC) FROM salaries;
-- Join with relational tables
WITH people AS (
SELECT elem->>0 AS name, elem->>1 AS email
FROM edn_q('[:find ?name ?email :where
[?e :person/name ?name] [?e :person/email ?email]]', '{}') AS q,
jsonb_array_elements(q->'results') AS elem
)
SELECT p.name, t.project_name, SUM(t.hours)
FROM people p JOIN time_entries t ON t.person_email = p.email
GROUP BY p.name, t.project_name;
See docs/SQL_INTEGRATION.md for the complete SQL integration guide and docs/EDN_TYPE.md for the EDN type reference.
Architecture
src/
lib.rs -- Extension entry point, Edn type definition, schema bootstrap SQL
types/
edn.rs -- Edn impl, edn_in/edn_out/edn_send/edn_recv
operators.rs -- EDN operators and accessor functions
functions/
transact.rs -- edn_t() - EDN transaction processing
query.rs -- edn_q(), mentat_explain() - Datalog to SQL compilation
pull.rs -- edn_pull(), mentat_pull_many() - Pull API
entity.rs -- mentat_entity() - Entity retrieval
schema.rs -- mentat_schema() - Schema introspection
stats.rs -- mentat_query_stats(), mentat_storage_stats(), mentat_slow_queries()
helpers.rs -- lookup_by_ident, entity_attrs, attribute_values, retract_entity
edn_helpers.rs -- batch, export_edn, import_edn, query_export_edn, export_all_edn
bootstrap.rs -- bootstrap_schema() - Core schema initialization
storage.rs -- SPI wrappers for entity allocation and lookups
cache.rs -- Schema cache (attribute ident resolution)
error.rs -- Error types with contextual messages
planner/
hooks.rs -- GUC parameters, optimizer hints, index suggestions
mod.rs -- Planner module
Storage Model
All data is stored in the mentat schema as EAVT datoms in a partitioned table:
- mentat.datoms – partitioned by
value_type_tag(LIST partitioning) with type-specific value columns (v_ref,v_long,v_text,v_bool,v_double,v_instant,v_keyword,v_uuid,v_bytes) - mentat.schema – attribute definitions
- mentat.idents – keyword to entity ID mappings
- mentat.transactions – transaction metadata
- mentat.fulltext – full-text search with tsvector/GIN
Eight indexes cover the four Datomic access patterns (EAVT, AEVT, AVET, VAET) with type-specific partial indexes for correct native comparisons.
Testing
cargo pgrx test pg16
The test suite includes 1,900+ tests covering transactions, queries, pulls, temporal operations, rules, aggregates, predicates, upserts, retractions, concurrency, and edge cases.
Configuration
GUC parameters for tuning:
| Parameter | Default | Description |
|---|---|---|
mentat.enable_optimizer_hints |
true |
Enable SET LOCAL optimizer hints |
mentat.default_work_mem |
64MB |
work_mem for complex queries |
mentat.max_result_rows |
0 |
Maximum result rows (0 = unlimited) |
Contributing
- Clone the repository and set up the environment (see Quick Start above).
- Make changes in
pg_mentat/src/. - Run
cargo clippy– the project enforces strict lints (no unwrap, no panic, no todo, no dbg, pedantic warnings). - Add or update tests.
- Run
cargo pgrx test pg16to validate. - Submit a PR against the
claudebranch.
Code Quality Standards
Resulttypes throughout;unwrap_usedandpanicare denied by clippy.- All public functions have doc comments.
- Parameterized queries via pgrx SPI (no SQL injection).
- Input validation: max nesting depth 100, max collection size 1M, max input 10MB.
License
Apache-2.0