Datalog Query Language

pg_mentat implements the Datomic dialect of Datalog. Queries are written in EDN (Extensible Data Notation) and passed to mentat_query() 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 mentat_query('[: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 mentat_query('[:find [?name ...]
                      :where [?e :person/name ?name]]', '{}');
-- Result: {"results":["Alice","Bob","Carol"]}

Tuple

Returns a single tuple (first match only):

SELECT mentat_query('[: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 mentat_query('[: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 mentat_query('[: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 mentat_query('[: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)

pg_mentat extends Datomic's predicate set with PostgreSQL's pattern matching:

[(like ?name "Ali%")]
[(ilike ?email "%@EXAMPLE.COM")]

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 LEFT JOIN ... COALESCE(v, default).

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

AggregateDescription
(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.

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 mentat_query(
  '[: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 mentat_query(
  '[: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]]]

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 mentat_query(
  '[: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 mentat_query(
  '[: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 mentat_query(
  '[: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:

KeyTypeDescription
"inputs"arrayPositional input values (first is always null for $)
"as_of"integerTransaction ID for point-in-time query
"since"integerTransaction ID for "changes since" query
"limit"integerMaximum result rows
SELECT mentat_query(
  '[:find ?name :where [?e :person/name ?name]]',
  '{"as_of": 1000, "limit": 10}'
);