# 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_mentat` does not require pg_tre. `mentat.has_pg_tre()` returns `true` when 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_join` next to `build_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.md` covers 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 helper `mentat.create_vector_index('<:attr>', 'hnsw'|'ivfflat', '')`. - 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/valueType` enum gets a new entry: `:db.type/vector`. Existing bootstrap entids 70–78 are taken; assign `:db.type/vector` entid 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-dim` meta-attribute (preferred) or schema `value_type` becoming `value_type | NULL` plus 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/geometry` and `: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 to `ST_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 function `mentat.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_bucket` available inside generated SQL for queries that group by time window. Integration shape: - Helper SQL `mentat.enable_timescale(target text)` where `target ∈ {'transactions', 'datoms_instant_new', 'all'}`. Calls `create_hypertable` with sensible chunk-time-interval defaults (probably one week). - A `mentat.transaction_window(?from, ?to)` Datalog where-fn that generates SQL using `time_bucket` when 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.datoms` still works once `datoms_instant_new` is 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`. Helper `mentat.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'); ``` partitions `datoms_instant_new` by tx range and drops partitions older than 90 days of tx history. - The narrow tables already have `(store_id, e, a, tx)` primary keys; `tx` is monotonic so range-partitioning by `tx` is 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 ""`. - 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 ""`: 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: 1. Open a tracking issue with the entry's section as the description. 2. Branch `feat/integration-`. 3. Build the SQL helper file `pg_mentat/sql/__helpers.sql` following the pattern in `11_pg_tre_helpers.sql`. 4. Add Datalog where-fn(s) in `pg_mentat/src/functions/query.rs` following the pattern around `build_fuzzy_match_join`. 5. Tests in `pg_mentat/src/_tests.rs`. Gate happy-path tests on the `mentat.has_()` check; assert error paths unconditionally. 6. Cookbook page at `docs/src/.md`. Link from `SUMMARY.md`. 7. Update this file: change `Status:` to `Done` with the commit hash and link to the cookbook page. 8. 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. See `docs/src/fuzzy-search.md`. Commit `9ad7650`. - **fuzzystrmatch** — phonetic and edit-distance scalar functions (`levenshtein`, `soundex`, `metaphone`, `daitch-mokotoff`) as Datalog where-fns. See `docs/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`). See `docs/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 AGPL `pg_search`. See `docs/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/vector` schema integration is a follow-up. See `docs/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 by `vector-near`'s per-row varying score. - **postgres_fdw cookbook** — cookbook page covering cross-database Datalog: foreign-table the remote `mentat` schema, push filters down via FDW, materialized-view caches, multi-tenant fan-out, and the `:in` clause pattern. Pure docs (no new where-fns) since postgres_fdw is built-in contrib. See `docs/src/postgres-fdw.md`. - **PgQue** (NikolayS/PgQue, Apache 2.0) — transactional event stream: one `mentat.tx` event per transaction, emitted from a deferred constraint trigger on `mentat.transactions`. Helpers: `mentat.pgque_emit_tx('queue')`, `mentat.pgque_disable_tx`, `mentat.pgque_register_consumer`. Pure-PL/pgSQL queue, no extensions, PG14+. See `docs/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`. See `docs/src/pg_infer.md`. - **PostGIS** (postgis.net, GPL-2.0+) — geospatial search via `(geom-near $ :attr "WKT" k)` (KNN by `ST_Distance`), `(geom-within $ :attr "WKT" radius)` (within-distance via `ST_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 from `geometry_columns` so input WKT is coerced to the column's projection. See `docs/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 a `mentat.create_property_graph_ddl` text generator. Forward-looking: PG19 ships SQL/PGQ; the integration is detect-only on PG13-PG18 today. See `docs/src/pg19_graph.md`. - **TimescaleDB** (Apache 2.0 OSS) — hypertable conversion for `mentat.transactions(tx_instant)` and `mentat.datoms_instant_new(v)`, plus retention policies. Helpers: `mentat.has_timescaledb`, `timescale_attach_transactions`, `timescale_attach_instant_datoms`, `timescale_set_transaction_retention`. See `docs/src/timescaledb.md`. - **pg_partman** (PostgreSQL license) — declarative partition management for `mentat.transactions` on `tx_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. See `docs/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. See `docs/src/pg_cron.md`.