Menu

Transactions

Overview

There are four transaction (write) operations:

Table 1. Write Operations
Operation Purpose

crux.tx/put

Write a version of a document

crux.tx/delete

Deletes the specific document at a given valid time

crux.tx/match

Check the document state against the given document

crux.tx/evict

Evicts a document entirely, including all historical versions

A document looks like this:

{:crux.db/id :dbpedia.resource/Pablo-Picasso
 :name "Pablo"
 :last-name "Picasso"}

In practice when using Crux, one calls crux.db/submit-tx with a sequence of transaction operations:

[[:crux.tx/put
 {:crux.db/id :dbpedia.resource/Pablo-Picasso
  :name "Pablo"
  :last-name "Picasso"}
 #inst "2018-05-18T09:20:27.966-00:00"]]

If the transaction contains pre-conditions, all pre-conditions must pass or the entire transaction is aborted. This happens at the query node during indexing, and not when submitting the transaction.

For operations containing documents, the id and the document are hashed, and the operation and hash is submitted to the tx-topic in the event log. The document itself is submitted to the doc-topic, using its content hash as key. In Kafka, the doc-topic is compacted, which enables later deletion of documents.

Valid IDs

The following types of :crux.db/id are allowed:

  • Keyword (e.g. {:crux.db/id :my-id} or {:crux.db/id :dbpedia.resource/Pablo-Picasso})

  • String (e.g. {:crux.db/id "my-id"})

  • Integers/Longs (e.g. {:crux.db/id 42})

  • UUID (e.g. {:crux.db/id #uuid "6f0232d0-f3f9-4020-a75f-17b067f41203"})

  • URI (e.g. {:crux.db/id #crux/id "mailto:crux@juxt.pro"})

  • URL (e.g. {:crux.db/id #crux/id "https://github.com/juxt/crux"}), including http, https, ftp and file protocols

  • Maps (e.g. {:crux.db/id {:this :id-field}}) (Note: see issue #362).

The #crux/id reader literal will take URI/URL strings and attempt to coerce them into valid IDs.

URIs and URLs are interpreted using Java classes (java.net.URI and java.net.URL respectively) and therefore you can also use these directly.

Operations

Put

Put’s a document into Crux. If a document already exists with the given :crux.db/id, a new version of this document will be created at the supplied valid time.

[:crux.tx/put
 {:crux.db/id :dbpedia.resource/Pablo-Picasso :first-name :Pablo} (1)
 #inst "2018-05-18T09:20:27.966-00:00"] (2)
1 The document itself. Note that the ID must be included as part of the document.
2 valid time

Note that valid time is optional and defaults to transaction time, which is taken from the Kafka log.

Crux puts into the past at a single point by default, so to overwrite several versions across a range in valid time, you can either submit a transaction containing several operations or supply a third argument to specify an end valid time. This period is inclusive-exclusive, such that the start of the validity period is included in the validity range, while the end is excluded.

Delete

Deletes a document at a given valid time. Historical versions of the document will still be available.

[:crux.tx/delete :dbpedia.resource/Pablo-Picasso
#inst "2018-05-18T09:21:52.151-00:00"]

Match

Match operations check the current state of an entity - if the entity doesn’t match the provided doc, the transaction will not continue. You can also pass nil to check that the entity doesn’t exist prior to your transaction.

[:crux.tx/match
 :ivan (1)
 {..} (2)
 #inst "2018-05-18T09:21:31.846-00:00"] (3)
1 Entity id
2 Document (or nil)
3 (optional) valid time

Evict

Evicts a document from Crux. Historical versions of the document will no longer be available.

[:crux.tx/evict :dbpedia.resource/Pablo-Picasso]

Transaction functions

Transaction functions are user-supplied functions that run on the individual Crux nodes when a transaction is being ingested. They can take any number of parameters, and return normal transaction operations which are then indexed as above. If they return false or throw an exception, the whole transaction will roll back.

Transaction functions can be used, for example, to safely check the current database state before applying a transaction, for integrity checks, or to patch an entity.

Transaction functions are created/updated by submitting a document to Crux with a crux.db/fn key. These functions are passed a 'context' parameter, which can be used to obtain a database value using db or open-db.

(crux/submit-tx node [[:crux.tx/put {:crux.db/id :increment-age
                                     ;; note that the function body is quoted.
                                     :crux.db/fn '(fn [ctx eid]
                                                    (let [db (crux.api/db ctx)
                                                          entity (crux.api/entity db eid)]
                                                      [[:crux.tx/put (update entity :age inc)]]))}]])

You can then invoke these transaction functions by submitting a :crux.tx/fn operation:

(crux/submit-tx node [[:crux.tx/put {:crux.db/id :ivan, :age 40}]])
(crux/submit-tx node [[:crux.tx/fn :increment-age :ivan]])

;; once those transactions have been indexed

(crux/entity (crux/db node) :ivan)
;; => {:crux.db/id :ivan, :age 41}

Events

You can subscribe to Crux events using the (crux.api/listen node event-opts f) function. Currently we expose one event type, :crux/indexed-tx, called when Crux indexes a transaction.

(require '[crux.api :as crux])

(crux/listen node {:crux/event-type :crux/indexed-tx, :with-tx-ops? true}
  (fn [ev]
    (println "event received!")
    (clojure.pprint/pprint ev)))

(crux/submit-tx node [[:crux.tx/put {:crux.db/id :ivan, :name "Ivan"}]])

prints:

event received!
{:crux/event-type :crux/indexed-tx,
 :crux.tx/tx-id ...,
 :crux.tx/tx-time #inst "...",
 :committed? true,
 :crux/tx-ops [[:crux.tx/put {:crux.db/id :ivan, :name "Ivan"}]]}

You can .close the return value from (crux.api/listen …​) to detach the listener, should you need to.

Speculative transactions

You can submit speculative transactions to Crux, to see what the results of your queries would be if a new transaction were to be applied. This is particularly useful for forecasting/projections or further integrity checks, without persisting the changes or affecting other users of the database.

You’ll receive a new database value, against which you can make queries and entity requests as you would any normal database value. Only you will see the effect of these transactions - they’re not submitted to the cluster, and they’re not visible to any other database value in your application.

We submit these transactions to a database value using with-tx:

(let [real-tx (crux/submit-tx node [[:crux.tx/put {:crux.db/id :ivan, :name "Ivan"}]])
      _ (crux/await-tx node real-tx)
      all-names '{:find [?name], :where [[?e :name ?name]]}
      db (crux/db node)]

  (crux/q db all-names) ; => #{["Ivan"]}

  (let [speculative-db (crux/with-tx db
                         [[:crux.tx/put {:crux.db/id :petr, :name "Petr"}]])]
    (crux/q speculative-db all-names) ; => #{["Petr"] ["Ivan"]}
    )

  ;; we haven't impacted the original db value, nor the node
  (crux/q db all-names) ; => #{["Ivan"]}
  (crux/q (crux/db node) all-names) ; => #{["Ivan"]}
  )

The entities submitted by the speculative :crux.tx/put take their valid time (if not explicitly specified) from the valid time of the db they were forked from.