Contents
- Postgres Extension Integrations — Plan
- Reference: the pg_tre integration (already shipped)
- Tier 1 — high-value, well-scoped, should land
- pgvector — :db.type/vector + (nearest-neighbor ...)
- PostGIS — :db.type/geometry + (within ...) / (near ...)
- TimescaleDB — mentat.transactions and datoms_instant_new as hypertables
- fuzzystrmatch — (soundex ...), (levenshtein ...), (metaphone ...)
- pg_partman — declarative partition management of retraction tombstones
- Tier 2 — useful and reasonably scoped, should land after Tier 1
- Tier 3 — interesting, lower priority
- Anti-list — extensions explicitly NOT worth integrating
- Process
- Done
Postgres Extension Integrations — Plan
This file enumerates Postgres extensions that pg_mentat could integrate with, in the same opt-in / soft-dependency shape as the already-shipped pg_tre integration. Each entry says what it adds, the integration shape (Datalog where-fn? new value type? helper SQL? index type on narrow tables?), license/build compatibility, effort, and whether it should land.
This is a planning document, not a marketing list. Extensions that don’t
add real Datalog value have been excluded. The file is the source of
truth for “what integrations does pg_mentat ship?” — when an integration
lands, mark it Done and link to the cookbook page.
Reference: the pg_tre integration (already shipped)
Use this as the template for everything below.
- Soft dependency.
CREATE EXTENSION pg_mentatdoes not require pg_tre.mentat.has_pg_tre()returnstruewhen the extension is loaded; helper functions error with a specific:db.error/*code pointing at install instructions when it isn’t. - One Datalog where-fn.
(fuzzy-match $ :attr "pattern" k)mirrors the shape of(fulltext ...)and slots into the same dispatch (build_fuzzy_match_joinnext tobuild_fulltext_join). - One helper SQL.
mentat.create_tre_index('<:attr>')builds a partial pg_tre index on the right narrow table. - Tests. Happy path and every error path, gated on
mentat.has_pg_tre()so the suite passes whether or not the test cluster has the extension installed. - Docs.
docs/src/fuzzy-search.mdcovers prerequisites, worked example, error table, explicit non-features.
Total cost: ~600 lines of Rust + SQL + docs + tests. One commit, one review pass. This is the cost ceiling for a single integration.
Tier 1 — high-value, well-scoped, should land
pgvector — :db.type/vector + (nearest-neighbor ...)
Status: Open. Effort: 2 weeks. Maintainer: pgvector/pgvector. License: PostgreSQL.
The single most valuable integration in 2026. Nothing else in the Postgres-Datalog space stores embeddings as datoms.
Integration shape:
- New value type
:db.type/vector. Encodes dimension as part of the attribute schema (:db/vector-dim 1536); rejects insert of mismatched vectors with:db.error/wrong-vector-dim. - New tenth narrow table
mentat.datoms_vector_new (store_id BIGINT, e BIGINT, a BIGINT, v vector, tx BIGINT, added BOOLEAN). Per-attribute HNSW or IVFFlat index via a new helpermentat.create_vector_index('<:attr>', 'hnsw'|'ivfflat', '<opts>'). - Datalog where-fn
(nearest-neighbor $ :attr ?vec k)returning the k nearest entities by cosine distance. Distance op is configurable per index (cosine / L2 / inner-product); pick from the attribute’s index metadata. - The
:db/valueTypeenum gets a new entry::db.type/vector. Existing bootstrap entids 70–78 are taken; assign:db.type/vectorentid 79 (free).
Hard parts:
- New value type means new code paths in
transact.rs(encoding),query.rs(the UNION subquery, the value-decode CASE, predicate comparison),pull.rs(decode), the dual-write trigger if it still exists by the time we land this. ~150–200 line touchpoints. - Storing dimension in the attribute schema requires either a new
:db/vector-dimmeta-attribute (preferred) or schemavalue_typebecomingvalue_type | NULLplus a new column. Pick the meta-attribute approach.
Done criteria:
SELECT mentat.t('[
{:db/ident :doc/embedding
:db/valueType :db.type/vector
:db/vector-dim 1536
:db/cardinality :db.cardinality/one}]');
SELECT mentat.create_vector_index(':doc/embedding', 'hnsw',
'm=16,ef_construction=64');
SELECT mentat.q('[:find ?e ?dist
:where [(nearest-neighbor $ :doc/embedding
[0.0,0.0,...] 10) [[?e ?dist]]]]');
PostGIS — :db.type/geometry + (within ...) / (near ...)
Status: Open. Effort: 2–3 weeks. Maintainer: OSGeo/postgis. License: GPL-2.0.
PostGIS is the most-used Postgres extension after pgcrypto. Geographic data in EAV is a common shape (places-of-business, asset locations, event coordinates).
Integration shape:
- New value types
:db.type/geometryand:db.type/geography. - New eleventh and twelfth narrow tables for each (or one shared with a type-tag column; pick whichever fits better with PostGIS’s GiST index semantics).
- Per-attribute GiST index via
mentat.create_spatial_index('<:attr>'). - Datalog where-fns:
(within $ :attr ?shape)— datoms whose value is within the given geometry. Compiles toST_Within.(near $ :attr ?point ?radius_m)— within radius (meters), uses<->operator on the GiST index for k-NN ordering when used with:order.(distance $ :attr ?point) ?d— bind ?d to the meters distance for use in predicates.
Hard parts:
- GPL-2.0 license. pg_mentat is Apache-2.0. PostGIS is a runtime dependency, not a derivative-work issue, so this is fine for the integration; document the license boundary.
- WKT vs WKB input. Decide one canonical form for EDN-encoded geometry
literals. Probably WKT inside a
#geom "POINT(1 2)"tagged literal, with a constructor functionmentat.geom('WKT')for SQL callers.
Done criteria: a cookbook page that does point-in-polygon queries mixing datoms and a GeoJSON polygon parameter.
TimescaleDB — mentat.transactions and datoms_instant_new as hypertables
Status: Open. Effort: 1 week. Maintainer: timescale/timescaledb. License: Apache-2.0 (TSL parts: TSL).
The transaction log and the instant-typed datom table are both naturally time-partitioned. Timescale-style hypertables would give:
- Automatic time-bucketed partitioning with proper chunk pruning.
- Continuous aggregates for time-series Datalog queries (count of retractions per hour, per attribute).
time_bucketavailable inside generated SQL for queries that group by time window.
Integration shape:
- Helper SQL
mentat.enable_timescale(target text)wheretarget ∈ {'transactions', 'datoms_instant_new', 'all'}. Callscreate_hypertablewith sensible chunk-time-interval defaults (probably one week). - A
mentat.transaction_window(?from, ?to)Datalog where-fn that generates SQL usingtime_bucketwhen the underlying table is a hypertable, falling back to a plain range scan otherwise. - No new value types or narrow tables. Pure operational integration.
Hard parts:
- Hypertables impose constraints on referenced rows (no foreign keys
pointing into them, etc.). Verify the dual-write VIEW on
mentat.datomsstill works oncedatoms_instant_newis a hypertable. - TimescaleDB’s TSL license vs the Apache-2.0 community edition. We should target the community edition to avoid licensing surprises.
Done criteria: enable_timescale('transactions') succeeds; a
time_bucket('1 day', tx_instant) query against mentat.transactions
runs against chunks; benchmark shows retraction-window queries scale
linearly with window size, not total tx count.
fuzzystrmatch — (soundex ...), (levenshtein ...), (metaphone ...)
Status: Open. Effort: 1–2 days. Maintainer: PostgreSQL contrib. License: PostgreSQL.
Built-in contrib module. PG13+. No shared_preload_libraries. No
runtime cost when not used. The cheap, no-friction alternative to
pg_tre for name-search and other typo-tolerant queries on PG13–17.
Integration shape:
- Three new Datalog where-fns:
[(levenshtein ?a ?b) ?d]— bind ?d to the integer edit distance between two text values. Pure function, can be used in predicates (e.g.[(< ?d 3)]for “within 2 edits”).[(soundex ?s) ?code]— bind ?code to the Soundex hash for name matching.[(metaphone ?s ?max) ?code]— bind ?code to the Metaphone phonetic encoding.
- All three compile to function calls with
extension_required=fuzzystrmatch. Helpermentat.has_fuzzystrmatch()returns true if the extension is loaded. - No index needed; these are scalar functions, called per row.
Hard parts: none. Smallest integration on the list.
pg_partman — declarative partition management of retraction tombstones
Status: Open. Effort: 1 week. Maintainer: pgpartman/pg_partman. License: PostgreSQL.
Operational rather than feature work. Long-running pg_mentat
deployments accumulate retracted-datom tombstones (added = false
rows) that grow indefinitely. pg_partman partitions a parent table by
range/list/hash and runs maintenance jobs to drop old partitions or
move them to cheap storage.
Integration shape:
- Per-narrow-table opt-in:
mentat.enable_partman(table_name, partition_by, retention). For example:SELECT mentat.enable_partman('datoms_instant_new', 'tx', '90 days');partitionsdatoms_instant_newby tx range and drops partitions older than 90 days of tx history. - The narrow tables already have
(store_id, e, a, tx)primary keys;txis monotonic so range-partitioning bytxis straightforward. - A pg_cron job (see entry below) runs
partman.run_maintenance()on a schedule.
Hard parts:
- Excision (the GDPR “forget me” path) and partition drops have to be coordinated; otherwise an excise call may target a partition that’s about to be dropped, or vice versa. Document the order.
- Existing data in non-partitioned narrow tables has to be migrated in-place — pg_partman has tooling for this but it’s not free.
Done criteria: partman maintenance on datoms_instant_new drops
partitions older than the configured retention; a benchmark shows
storage growth flattens after the retention window.
Tier 2 — useful and reasonably scoped, should land after Tier 1
pg_trgm — (similar-to ...) where-fn
Status: Open. Effort: 3–5 days. License: PostgreSQL (contrib).
Built-in. Trigram similarity (set-based, not edit-distance). Different semantics from pg_tre and from fuzzystrmatch’s Levenshtein — ranks by overlap of trigrams, not by edit distance. Useful for “rows whose attribute is kind of like this string” queries.
Shape:
- [(similar-to $ :attr "needle" threshold) [[?e ?val ?score]]].
- Helper mentat.create_trgm_index('<:attr>') creates a partial
gin_trgm_ops or gist_trgm_ops index on the appropriate narrow
table.
- Coexists with (fulltext ...) and (fuzzy-match ...). Document the
distinction in docs/src/text-search-comparison.md.
pg_jsonschema — :db/jsonschema constraint
Status: Open. Effort: 3–5 days. Maintainer: supabase/pg_jsonschema. License: Apache-2.0.
Validates JSON against a JSON Schema. pgrx-based, like pg_mentat.
Shape:
- New attribute-schema option :db/jsonschema "<schema-text>".
- On mentat_transact, when an attribute has :db/jsonschema set and
the value is a string, validate the parsed JSON against the schema
before inserting. Reject with :db.error/jsonschema-violation.
- Document why this isn’t :db/predicate "<EDN expression>": JSON
Schema is an industry standard with well-defined semantics, EDN
predicates aren’t.
pg_cron — scheduled compaction and maintenance
Status: Open. Effort: 2–3 days. Maintainer: citusdata/pg_cron. License: PostgreSQL.
Schedule periodic operations: VACUUM on hot narrow tables, partition maintenance for pg_partman, retraction GC for tombstoned datoms older than retention, refresh of materialized monitoring views.
Shape:
- Helper SQL mentat.schedule_maintenance(name, cron_expr, sql_body)
that registers a pg_cron job in the right database.
- Built-in jobs: mentat.scheduled_vacuum,
mentat.scheduled_excision_gc,
mentat.scheduled_partman_run (registers the partman job).
- Soft dep, gated on mentat.has_pg_cron().
postgres_fdw + cookbook — query datoms across PG instances
Status: Open. Effort: 2 days (mostly docs). License: PostgreSQL (contrib).
Built-in. No code change in pg_mentat needed; this is a cookbook page
showing how to set up a foreign server pointing at another pg_mentat
instance, import its datoms_*_new tables as foreign tables, and run
Datalog-shaped queries that JOIN them.
Shape:
- docs/src/cookbook-multi-instance-fdw.md.
- The mentat_query function operates on local tables only; cross-
instance queries are written as plain SQL that JOINs local Datalog
results with foreign datom tables.
- Optional helper mentat.import_remote_store(server, store_name) that
generates the CREATE FOREIGN TABLE statements for one remote store.
h3-pg — geographic indexing for PostGIS users
Status: Open. Effort: 2 days, only after PostGIS lands. Maintainer: zachasme/h3-pg. License: Apache-2.0.
Uber’s H3 hexagonal hierarchical spatial index. Pairs with PostGIS for fast point-in-region queries at scale. Niche but well-loved.
Shape:
- Helper mentat.h3_index('<:attr>', resolution) that adds an H3 cell
index to a :db.type/geometry attribute.
- Datalog where-fn (in-h3-cell $ :attr ?cell).
Tier 3 — interesting, lower priority
pg_search (ParadeDB) — Tantivy-backed BM25 full-text
Status: rejected on license grounds. pg_search is AGPL-3.0; this
is a non-starter for many commercial deployments. Use rum
instead — it’s PostgreSQL-licensed and provides positional ranked
search that is close to BM25 in spirit. See docs/src/rum.md.
pg_duckdb — DuckDB columnar engine inside Postgres
Effort: 2–3 weeks. License: MIT.
Real value for analytical workloads on the historical datom log. Speculative until pg_duckdb’s API stabilizes; today (mid-2026) it’s still moving fast. Re-evaluate annually.
citus — sharded multi-store
Effort: 4–6 weeks. License: AGPL-3.0 (some pieces) / PostgreSQL.
The path to “datoms over many machines.” Requires significant rework of the narrow-table primary keys to include a shard-key column. Phase A-grade work; not a single integration. Tracked here so it doesn’t get re-discovered every six months.
pglogical / wal2json — CDC of the datom log
Effort: 1 week (smoke test only). License: PostgreSQL-style.
The narrow tables are append-mostly (retractions are also INSERTs with
added = false); they replicate cleanly via either path. The work is
operational documentation more than code. Already on the existing
roadmap as “Phase 6 CDC.”
plv8 / plpython3u — user-defined Datalog where-fns
Effort: 2 weeks. License: PostgreSQL.
Lets users define custom predicates and functions in JS or Python. The
sandboxing story is the question — plpython3u is u (untrusted) for
a reason. Probably not worth the security review unless someone is
asking.
pg_stat_statements — query observability
Effort: already integrated as opt-in. License: PostgreSQL.
The Phase 2 benchmark harness uses pg_stat_statements when available to capture top-10 slowest statements. Done.
pgaudit — DDL/DML audit logging
Effort: 1 day (cookbook). License: PostgreSQL.
pg_mentat already has datom-level audit semantics built in. pgaudit
adds defense-in-depth at the SQL level (catches direct writes to
narrow tables that bypass mentat_transact). Cookbook page is enough.
Anti-list — extensions explicitly NOT worth integrating
| Extension | Reason |
|---|---|
pgcrypto |
Used internally where useful; nothing to expose to Datalog. |
pg_uuidv7 / pg_idkit |
:db.type/uuid exists; users supply their own UUIDs. |
zhparser and other tsvector-language plugins |
One-line config of :db/fts-language; not an integration. |
pgrouting |
Datalog rules already do recursive graph traversal. |
pg_squeeze / pg_repack |
Operational tools; not Datalog-visible. |
pg_anonymizer |
Datomic-style retraction + RLS already cover the use case. |
pgsql-http / pg_net |
Side-effecting from queries is anti-Datalog by design. |
Process
When implementing an entry:
- Open a tracking issue with the entry’s section as the description.
- Branch
feat/integration-<name>. - Build the SQL helper file
pg_mentat/sql/<NN>_<name>_helpers.sqlfollowing the pattern in11_pg_tre_helpers.sql. - Add Datalog where-fn(s) in
pg_mentat/src/functions/query.rsfollowing the pattern aroundbuild_fuzzy_match_join. - Tests in
pg_mentat/src/<name>_tests.rs. Gate happy-path tests on thementat.has_<extension>()check; assert error paths unconditionally. - Cookbook page at
docs/src/<name>.md. Link fromSUMMARY.md. - Update this file: change
Status:toDonewith the commit hash and link to the cookbook page. - PR. Squash. Push. The integration ships exactly as opt-in.
Do not bundle multiple integrations in one PR. Each integration is independently reviewable; bundling rebuilds the same merge-conflict problem we keep running into elsewhere.
Done
- pg_tre — approximate-regex search via the
(fuzzy-match ...)where-fn. Seedocs/src/fuzzy-search.md. Commit9ad7650. - fuzzystrmatch — phonetic and edit-distance scalar functions
(
levenshtein,soundex,metaphone,daitch-mokotoff) as Datalog where-fns. Seedocs/src/fuzzystrmatch.md. PG13+, no preload required. - pg_trgm — trigram-similarity matching via the
(similar-to ...)where-fn, plus partial-GIN index helpers (mentat.create_trgm_index,mentat.drop_trgm_index). Seedocs/src/pg-trgm.md. PG13+, no preload required. - rum (postgrespro/rum, PostgreSQL license) — BM25-style ranked
fulltext via the
(rum-fulltext $ :attr "term")where-fn, with partial-RUM index helpers (mentat.create_rum_fulltext_index,mentat.drop_rum_fulltext_index). The permissive alternative to ParadeDB’s AGPLpg_search. Seedocs/src/rum.md. - pgvector (partial) — K-nearest-neighbor search via the
(vector-near $ :attr "[...]" k [op])where-fn, plus aux-table helpers (attach_vector_attribute,set_vector,del_vector,create_hnsw_vector_index). Side-table design; full:db.type/vectorschema integration is a follow-up. Seedocs/src/pgvector.md. - (query-side fix bundled with pgvector) — the FTS-style join
builders (
fulltext,fuzzy-match,similar-to,rum-fulltext,vector-near) now properly JOIN their entity binding to subsequent patterns instead of relying on DISTINCT to mask cartesian products. Fixes a latent bug exposed byvector-near’s per-row varying score. - postgres_fdw cookbook — cookbook page covering cross-database
Datalog: foreign-table the remote
mentatschema, push filters down via FDW, materialized-view caches, multi-tenant fan-out, and the:inclause pattern. Pure docs (no new where-fns) since postgres_fdw is built-in contrib. Seedocs/src/postgres-fdw.md. - PgQue (NikolayS/PgQue, Apache 2.0) — transactional event
stream: one
mentat.txevent per transaction, emitted from a deferred constraint trigger onmentat.transactions. Helpers:mentat.pgque_emit_tx('queue'),mentat.pgque_disable_tx,mentat.pgque_register_consumer. Pure-PL/pgSQL queue, no extensions, PG14+. Seedocs/src/pgque.md. - pg_infer (codeberg.org/gregburd/pg_infer, Apache 2.0) —
model-knowledge search via the experimental pg_infer extension
(PG18+). Where-fns:
(infer-near $ :attr "text" k [:model])for top-K KNN ranked by model knowledge using<~>,(infer-similar a b)for scalar similarity,(infer-implies a b)for directional implication, plus three set-returning verbs(infer-walk "prompt" top),(infer-describe "entity"),(infer-predict "prompt" top)with relation bindings. Helpers:mentat.has_pg_infer,mentat.create_infer_index,mentat.drop_infer_index. Seedocs/src/pg_infer.md. - PostGIS (postgis.net, GPL-2.0+) — geospatial search via
(geom-near $ :attr "WKT" k)(KNN byST_Distance),(geom-within $ :attr "WKT" radius)(within-distance viaST_DWithin),(geom-contains $ :attr "WKT")(ST_Contains),(geom-intersects $ :attr "WKT")(ST_Intersects). Aux-table helpers:mentat.attach_geometry_attribute,set_geometry,del_geometry,create_gist_geometry_index,detach_geometry_attribute. SRID auto-detected fromgeometry_columnsso input WKT is coerced to the column’s projection. Seedocs/src/postgis.md. - PG19 SQL/PGQ (Property Graph Queries) — vertex / edge view
helpers (
mentat.create_vertex_view,create_edge_view,drop_*) that map narrow datom storage onto SQL/PGQ-compatible tables, plus amentat.create_property_graph_ddltext generator. Forward-looking: PG19 ships SQL/PGQ; the integration is detect-only on PG13-PG18 today. Seedocs/src/pg19_graph.md. - TimescaleDB (Apache 2.0 OSS) — hypertable conversion for
mentat.transactions(tx_instant)andmentat.datoms_instant_new(v), plus retention policies. Helpers:mentat.has_timescaledb,timescale_attach_transactions,timescale_attach_instant_datoms,timescale_set_transaction_retention. Seedocs/src/timescaledb.md. - pg_partman (PostgreSQL license) — declarative partition
management for
mentat.transactionsontx_instant. Helpers:mentat.has_pg_partman,partman_attach_transactions,partman_set_transaction_retention,partman_run_maintenance. Refuses one-time table conversion to prevent silent data loss; manual conversion path documented. Seedocs/src/pg_partman.md. - pg_cron (PostgreSQL license) — scheduled-maintenance wrapper.
Helpers:
mentat.has_pg_cron,cron_schedule,cron_unschedule, plus convenience schedulers for partman maintenance and narrow- datom-table VACUUM. Seedocs/src/pg_cron.md.