datomic 2026-08-26

Is it possible to disallow the execution of arbitrary code in Datomic queries? The expressiveness of Datomic allows me to write queries that executes arbitrary code in queries. For example:

(d/q '[:find [?x]
       :where [(clojure.core/load-string "(+ 1 2)") ?x]]
     db) 
=> 3
That's not a problem when I, the person, create the query. I can just make sure to not write malicious queries. But it's a real risk if I want to allow an AI to create and execute queries in a context that is truly read-only and side effect free. The agent could easily inject side effects into the query, like reading/writing from disk, make network requests etc. I would like to be able to create a fn that could be hooked up to an agent and that looks something like this:
(defn safe-run-query [find where]
  (d/q [:find (edn/read-string find)
        :where (edn/read-string where)]
       db))
... that allows and executes queries devoid of side effects, but not ones with side effects. For example:
(safe-run-query "[?x]" "[(+ 1 2) ?x]") 
=> 3

(safe-run-query "[?x]" "[(java.lang.System/getenv \"SOME_SECRET\") ?x]")
=> 
One possible way to mitigate that is to traverse the whole :where clause, cross reference each symbol in the query to an allowlist of known side effect free fns. That should work, but it's easy to get wrong and puts a lot of responsibility on me as the implementer of the allowlisting. Another is to simply export data from Datomic into some other DB, that doesn't support side effects in its query language, and run queries against that DB instead. I'm aware that read-only connections are possible since Datomic v1.0.7622, but that doesn't solve the side effect problem. Is there any other way to guarantee that a query is read-only and side effect free that I'm missing?

There is no foolproof way to prevent arbitrary execution within queries in Datomic. This would be a feature request (and other users have requested such a feature, so I already have a story for it and can add your interest to that story.) Query functions are a powerful tool and thus have tradeoffs, this potential sharp edge is one of them. Some strategies to mitigate the issue include, writing a wrapper api for d/q that does what you need, but agents can probably work around that. Another option is standing up a separate peer applications where agents can run their agent-supplied queries and lock it down at OS/container level. This is a benefit of the ability have n peers in Datomic and if an agent decides to go rogue you should only end up messing up other agents and not your production users. You can ensure that environment has fewer secrets or none, is self contained, and perhaps you make it short lived with cycles to reduce the chance of an incident. This approach might become especially useful in combination with using a read-only database (specifically an s3 backup location) as you can also guarantee that these agents will not make changes to the DB as you cannot transact to a read-only backup. I will raise your request around arbitrary code execution in query at our next story review, Friday and will circle back if dev has other ideas.

Is the threat model here that you don't trust the AI generated query? If so, I would treat it the same as untrusted user input in which you have to sanitize the query yourself. Luckily, you can traverse the query as data with Clojure and sanitize/assess it's safety.

for such traversal though, it would be great, if the vector format datalog query to map format converter function would be public officially, so one could transform it programmatically.

It's a common pattern in Datomic to issue a transaction followed by a query to determine the impact of the transaction. The idiomatic way to achieve this is to include the :tx-data of the transaction result as a relation input to the query. For example

(let [{:keys [db-after tx-data] :as tx-result} (d/transact conn {:tx-data my-tx-data})
      q '{:find [(sum ?ga)]
          :in [$ [[?e ?a ?v ?tx ?added] ...]]
          :where [[(true? ?added)]
                  [?a0 :db/ident :st.purchase-invoice/gross-amount]
                  [?e ?a0 ?v]
                  [?e ?a0 ?ga]]}]
  (d/q {:query q :args [db-after tx-data])))
My understanding is that this style and brevity is the norm with the peer client. It also works with db-local and might even work with the ion client. But it does not work with the cloud client because the transaction result returns a collection of datomic.client.impl.shared.datom.Datom instances. These instances cannot be handled by the client's transit serializers. The java type is trivially converted to a five-tuple by a helper function pair:
(defn expand-datom
  "Convert the Datom object `datom` to a 5-tuple of [e a v t added?]."
  [[e a v t added? :as datom]] [e a v t added?])

(defn dehydrate-datoms [datoms] (sequence (map expand-datom) datoms))
The example I gave above needs an intermediate step now to convert the :tx-data value into simple 5-tuple vectors. It just seems like an oversight that the native type returned by the cloud client can't be passed directly back into a query.

➕ 1

both of the following classes can be returned by the various datomic query implementations: 1. datomic.client.impl.shared.datom/Datom (deftype Datom [e a v tx added] Object ... ILookup ... Counted ... Indexed ...) 2. datomic.core.db/Datum public final class Datum implements IDatumImpl, Datom, ILookup, IDatum, Counted, MemorySize, Indexed, IType {... since they both support ILookup, so your expand-datom is effectively (juxt :e :a :v :tx :added).

That is a nice clean impl.

thx for showing this technique. i was aware, it's possible to pass in a datom set in place of data sources into a datalog query in the peer api, but i haven't realized, that i can do a similar thing in the client api too, just not in place of data source, but as regular datalog query args. what we do instead of such post analysis is running a d/with 1. for the sake of input validation 2. detect would-be-empty transactions, so we can keep a submit button disabled on the UI, for example the drawback of using d/with on a query group, is that the transaction are evaluated on the query group too, so any transaction functions they use, must be allow-listed in the ion-config.edn on the query group too.

Our use case might be a bit different in that we are less interested in the "100% redundant tx" versus "100% effective tx" (or anything on that spectrum) in a static sense but rather the net impact of transaction functions when faced with concurrent processes evolving the database. A with-db transaction + following query would give a hypothetical answer that could change when the transaction is actually applied (due to concurrency of course). I love that Datomic is very explicit in the transaction result of what actually happened and that it is possible to write datalog queries across that "delta" and the resulting database.

1