Database Functions

Datomic supports functions as first-class values in the database. This simple yet powerful facility enables:

  • Atomic transformation functions in transactions
  • Integrity checks and constraints
  • Predicates and generative functions for queries
  • Database-driven dynamic code distribution to peers
  • and much more - your imagination is the limit!

There is a video introducing database functions.

Database Function Basics

Create a function

You can write database functions in Java or Clojure. A database function may have up to 10 arguments. You can programmatically create a database function by first creating a map with information about the function - its language, parameters and code, then supplying this map to the Peer.function() method. Alternatively, you can embed a function definition in a transaction script by using the #db/fn{…} literal, which tags a similar map. All database functions take and return Objects.

Database functions will need any dependencies declared. When database functions are installed, you can optionally provide two keyword parameters, :imports and :requires, for Clojure dependencies. For Java dependencies, the method body set for :code may begin with one or more import statements. This functionality is documented in the Clojure API and the javadoc.

Installing a function

You install a function simply by adding it as an attribute of an entity. There is already an attribute of the correct (:db.type/fn) type - :db/fn. Normally you will also add a :db/ident attribute on the function entity to serve as its name, as well as a :db/doc string. When a function is added to the database, its language and code are stored.

Using functions

The function object that you get from calling Peer.function() is the same thing that you will get when retrieving the :db/fn attribute. It is an object that will implement datomic.functions.Fn, as well as the one of datomic.functions.FnN matching its arity. In addition, for Clojure users, it will implement clojure.lang.IFn. This object will dynamically compile itself the first time it is invoked. Subsequent calls will be as fast as any compiled Java code - the calls are neither interpreted nor reflective. To invoke a function, simply call invoke() on it. You can call database functions written in either language from any JVM language with interop support.

Transaction Functions

Datomic will invoke database functions as part of transaction processing. Functions written for this purpose are called transaction functions.

Creating and installing a transaction function

Creating and installing a transaction function is exactly like creating any database function, but the function must meet certain requirements:

  • Transaction functions must be pure functions, i.e. free of side effects.
  • A transaction function must expect to be passed a database value as its first argument.

This is to allow transaction function to issue queries etc.

  • Additionally, a transaction function must return transaction data

i.e. data in the same form as expected by Connection.transact().

Using transaction functions

'Calls' to your transaction functions are just ordinary transaction entries (lists) like the built-in ones for :db/add and :db/retract:

;;add an entity called :foo
  :db/ident :foo}]

;;add a transaction function called add-doc
  :db/ident :add-doc
  :db/fn #db/fn {:lang "java"
                 :params [db e doc]
                 :code "return list(list(\":db/add\", e, \":db/doc\", doc));"}}]

;;calls add-doc in a transaction
[[:add-doc :foo "this is foo's doc"]]
conn.db().entity(":foo").get(":db/doc") -> "this is foo's doc"

In this example 3 transactions are run. The first creates a new entity with an ident of :foo. The second adds a new transaction function to the database called :add-doc. :add-doc takes the db (which it doesn't use), an entity e, and a doc string doc. It returns transaction data that looks like this:

[[:db/add e :db/doc doc]]

Note that, by default, the methods (e.g. list()) of Util and Peer are already statically imported.

The third transaction calls :add-doc to add docs to :foo.

Processing transaction functions

The transaction processor will lookup the function in its :db/fn attribute, and then invoke it, passing the value of the db (currently, as of the beginning of the transaction), followed by the arguments - e.g. f.invoke(db, :foo, "this is foo's doc"). It will then take the result of the call (which is a list of transaction data), and 'splice' it into the transaction where the call was made. The result might contain several transaction entries, and some of them may be transaction function calls. The transaction processor will call these in turn, until the expansion consists only of :db/adds and :db/retracts.

Uses for transaction functions

Transaction functions run on the transactor inside of transactions, and thus can atomically analyze and transform database values. You can use them to ensure atomic read-modify-update processing, and integrity constraints. (To abort a transaction, simply throw an exception). If you frequently need to create entities with a particular 'shape' you can make constructor-like transaction functions. A transaction function can issue queries on the db value it is passed, and can perform arbitrary logic in the programming language. Note, however, that transaction functions must be pure functions and cannot be used to produce effects on the transactor.

Limitations of transaction functions

  • Transaction functions must be pure functions and cannot be used to produce effects on the transactor.
  • Transaction functions are serialized by design. To achieve best performance, limit the work of transaction functions to only things that require transaction-time access to the current value of the database.