Datalog Query Language

pg_mentat implements the Datomic dialect of Datalog. Queries are written in EDN (Extensible Data Notation) and passed to edn_q() as text strings.

Query Structure

A query has the general form:

[:find <find-spec>
 :in <input-bindings>       ;; optional
 :with <with-vars>          ;; optional
 :where <clauses>]

Find Specifications

The :find clause determines the shape of the result.

Relation (default)

Returns a collection of tuples (array of arrays):

SELECT edn_q('[:find ?name ?age
                      :where
                      [?e :person/name ?name]
                      [?e :person/age ?age]]', '{}');
-- Result: {"columns":["name","age"],"results":[["Alice",30],["Bob",25]]}

Collection

Returns a flat array of values (single variable, all matches):

SELECT edn_q('[:find [?name ...]
                      :where [?e :person/name ?name]]', '{}');
-- Result: {"results":["Alice","Bob","Carol"]}

Tuple

Returns a single tuple (first match only):

SELECT edn_q('[:find [?name ?age]
                      :where
                      [?e :person/name ?name]
                      [?e :person/age ?age]]', '{}');
-- Result: {"results":["Alice",30]}

Scalar

Returns a single value (first match, first variable):

SELECT edn_q('[:find ?name .
                      :where [?e :person/name ?name]]', '{}');
-- Result: {"results":"Alice"}

Where Clauses

Basic Pattern

A pattern matches datoms against the EAV model:

[?entity :attribute ?value]
[?entity :attribute "literal"]
[?entity :attribute]          ;; value ignored (existence check)

Each position can be a variable (?x), a literal, or blank (_).

SELECT edn_q('[:find ?e
                      :where
                      [?e :person/name "Alice"]]', '{}');

Multiple Patterns (Implicit AND)

Multiple patterns in :where are joined – a variable must unify across all patterns:

SELECT edn_q('[:find ?name ?email
                      :where
                      [?e :person/name ?name]
                      [?e :person/email ?email]]', '{}');

NOT Clauses

Exclude results matching a pattern:

[:find ?name
 :where
 [?e :person/name ?name]
 (not [?e :person/age 25])]

Compiles to NOT EXISTS (subquery).

NOT-JOIN

Explicit variable binding for NOT:

[:find ?name
 :where
 [?e :person/name ?name]
 (not-join [?e]
   [?e :person/retired true])]

OR Clauses

Match any of several alternatives:

[:find ?name
 :where
 [?e :person/name ?name]
 (or [?e :person/role "admin"]
     [?e :person/role "superuser"])]

Compiles to UNION.

OR-JOIN

Explicit variable binding for OR:

[:find ?name
 :where
 [?e :person/name ?name]
 (or-join [?e]
   [?e :person/active true]
   [?e :person/admin true])]

Predicates

Predicates filter results using comparison operators. They appear in the :where clause with function-call syntax:

[(> ?age 21)]
[(< ?price 100.0)]
[(<= ?start ?end)]
[(>= ?score 90)]
[(= ?status "active")]
[(!= ?role "banned")]

Arithmetic

Arithmetic expressions bind their result to a variable:

[:find ?name ?total
 :where
 [?e :person/name ?name]
 [?e :order/price ?price]
 [?e :order/quantity ?qty]
 [(* ?price ?qty) ?total]
 [(> ?total 1000)]]

Supported operators: +, -, *, /.

String Predicates (Extension)

Every backend (pg_mentat, the embedded engine, DuckDB) extends Datomic’s predicate set with pattern matching and Clojure’s string tests:

[(like ?name "Ali%")]               ; case-sensitive; % any run, _ one character
[(ilike ?email "%@EXAMPLE.COM")]    ; case-insensitive
[(like ?code "50\\%")]             ; \ escapes % and _
[(starts-with? ?name "Al")]
[(ends-with? ?file ".edn")]
[(includes? ?title ?word)]

The first argument is a string value (a keyword value matches on its text without the leading :, e.g. "k/alpha"). The second is a string: a constant or a bound variable. On the embedded engine and DuckDB a like / ilike pattern must be a constant or an :in value. On the embedded engine ilike folds ASCII letters only ("É" does not match "é"); PostgreSQL and DuckDB fold all of Unicode.

Type Safety

Predicates that compare incompatible types (e.g., comparing a string variable against an integer) produce an empty result set rather than an error. This matches Datomic’s behavior.

Query Functions

ground

Bind a constant value to a variable:

[:find ?name
 :where
 [(ground 42) ?answer]
 [?e :person/age ?answer]
 [?e :person/name ?name]]

get-else

Return a default value when an attribute is missing:

[:find ?name ?age
 :where
 [?e :person/name ?name]
 [(get-else $ ?e :person/age 0) ?age]]

Compiles to a correlated COALESCE((SELECT v ... WHERE e = ?e AND a = :attr), default), so it works with any number of patterns. As in Datomic, it is an error on a cardinality-many attribute. As-of and since queries read the value at that basis. The result can be projected, aggregated, and used in predicates and arithmetic ([(get-else $ ?e :a 0) ?x] [(> ?x 1)]). The embedded SQLite and DuckDB backends support get-else and missing? too (1.17.0).

missing?

Test that an entity lacks an attribute:

[:find ?name
 :where
 [?e :person/name ?name]
 [(missing? $ ?e :person/email)]]

Compiles to NOT EXISTS subquery.

Aggregates

Aggregate functions wrap find variables:

[:find (count ?e)
 :where [?e :person/name]]

[:find ?dept (avg ?salary) (max ?salary)
 :where
 [?e :person/department ?dept]
 [?e :person/salary ?salary]]

Supported Aggregates

Aggregate Description
(count ?x) Count of values
(count-distinct ?x) Count of distinct values
(sum ?x) Sum of numeric values
(avg ?x) Average of numeric values
(min ?x) Minimum value
(max ?x) Maximum value
(sample N ?x) Random sample of N values

Non-aggregated variables in the find spec become the GROUP BY columns automatically.

Over no rows, count and count-distinct return 0 and the other aggregates return nothing: a scalar or tuple find gives nil, a collection or relation find gives no values.

Rules

Rules are named, reusable query fragments. Define them in the :in clause with the % symbol and provide rule definitions as the corresponding input.

Basic Rules

SELECT edn_q(
  '[:find ?name
    :in $ %
    :where (adult ?e)
           [?e :person/name ?name]]',
  '{"inputs": [null, [["(adult ?person)", "[?person :person/age ?age]", "[(>= ?age 18)]"]]]}'
);

Recursive Rules

Rules can reference themselves for graph traversal:

-- Find all ancestors (transitive parent-of)
SELECT edn_q(
  '[:find ?ancestor
    :in $ % ?person
    :where (ancestor ?person ?ancestor)]',
  '{"inputs": [null, [
    ["(ancestor ?p ?a)", "[?p :person/parent ?a]"],
    ["(ancestor ?p ?a)", "[?p :person/parent ?mid]", "(ancestor ?mid ?a)"]
  ], 10001]}'
);

Recursive rules compile to PostgreSQL WITH RECURSIVE CTEs with a depth limit controlled by the mentat.max_recursion_depth GUC (default 100).

Multi-head Rules

Multiple rule definitions with the same name act as alternatives (logical OR):

[[(related ?a ?b) [?a :person/friends ?b]]
 [(related ?a ?b) [?a :person/colleagues ?b]]]

Rule heads take variables only

As in Datomic, every argument of a rule head is a variable. A constant in the head, such as [(isa ?e :role/dev) [?e :person/role :role/dev]], is rejected with :db.error/invalid-rule. To match a constant, bind a variable in the head and pass the constant where the rule is invoked:

[:find ?e
 :rules [[(isa ?e ?r) [?e :person/role ?r]]]
 :where (isa ?e :role/dev)]

A rule invocation takes variables and constants (numbers, strings, keywords, booleans, instants, uuids).

Input Bindings

The :in clause declares external parameters. The implicit first input is always $ (the database).

Scalar Binding

[:find ?name
 :in $ ?min-age
 :where
 [?e :person/name ?name]
 [?e :person/age ?age]
 [(>= ?age ?min-age)]]
SELECT edn_q(
  '[:find ?name :in $ ?min-age :where [?e :person/name ?name] [?e :person/age ?age] [(>= ?age ?min-age)]]',
  '{"inputs": [null, 21]}'
);

Collection Binding

Match against a set of values:

[:find ?name
 :in $ [?city ...]
 :where
 [?e :person/name ?name]
 [?e :person/city ?city]]
SELECT edn_q(
  '[:find ?name :in $ [?city ...] :where [?e :person/name ?name] [?e :person/city ?city]]',
  '{"inputs": [null, ["NYC", "SF", "LA"]]}'
);

Compiles to IN (...) or a values join.

Tuple Binding

Bind multiple variables at once:

[:find ?name
 :in $ [?first ?last]
 :where
 [?e :person/first-name ?first]
 [?e :person/last-name ?last]
 [?e :person/name ?name]]

Relation Binding

Pass a table of values:

[:find ?name ?score
 :in $ [[?name ?score]]
 :where
 [?e :person/name ?name]
 [(> ?score 80)]]
SELECT edn_q(
  '[:find ?name ?score :in $ [[?name ?score]] :where [?e :person/name ?name] [(> ?score 80)]]',
  '{"inputs": [null, [["Alice", 95], ["Bob", 72], ["Carol", 88]]]}'
);

Compiles to a VALUES join.

With Clause

The :with clause includes variables in grouping without including them in results. This prevents unwanted deduplication:

[:find (count ?name)
 :with ?e
 :where [?e :person/name ?name]]

Without :with ?e, duplicate names would be counted once. With it, each entity contributes separately.

Query Options

Options are passed in the JSON inputs parameter:

Key Type Description
"inputs" array Positional input values (first is always null for $)
"asOf" integer The database as of this transaction: values retracted at or before it are hidden
"since" integer The current datoms transacted after this transaction (Datomic’s since); a history pattern [?e ?a ?v ?tx ?added] sees every datom after it, retractions too
"history" boolean Every pattern reads the log, assertions and retractions
"limit" integer Maximum result rows
"offset" integer Skip this many result rows

asOf and since may be combined.

SELECT edn_q(
  '[:find ?name :where [?e :person/name ?name]]',
  '{"asOf": 1000, "limit": 10}'
);