Setting Null Values in Datomic

Datomic does not allow the storage of null values. We must reconcile which values were edited and which values were emptied. The emptied values must be a retracted to set to null.

Why Do Null Values Not Exist in Datomic?

It took a few moments to fully understand why it is that Datomic does not allow setting a value to null. If you do try, this is the exception you receive.

IllegalArgumentExceptionInfo :db.error/nil-value Nil is not a legal value datomic.error/arg (error.clj:55)

Looking at the development resources for Datomic and the section on Implications, it states:

Most Datomic writes are of tree nodes. These writes are compatible with eventually consistent storage, because the semantics of immutable values are beautifully simple: In an immutable system with no updates, there are only two possibilities:
- a value is present
- a value is not present yet
https://docs.datomic.com/on-prem/acid.html#implications

The important pieces to understand are the bullet points. A value must either exist or not; it cannot be null. Thinking in terms of datoms and entities, an entity either has a value for an attribute or the attribute does not exist for the entity. The entity cannot have an attribute with a value of null.

So what if we need to set a value to null?

How to Set Null Values

Let’s say that John fills out a form and includes his firstname and lastname. We would save this to Datomic as such.

Retracting Values Programmatically

When John submits the form to remove his lastname, the system can’t hardcode the old value to build the retract statement. Here’s how you might go about finding the correct value and retracting when you need to. This code will not retract values which weren’t removed.

(defnedit-or-create-user-txn[params]"Returns a transaction for creating a new entity and/or
updating attributes of an existing entity."(let[user-entity(find-user-entity(:idparams))id(ifuser-entity(:db/iduser-entity)(datomic/tempid:db.part/user-1))edited(edited-fieldsidparams)retracted(retracted-fieldsuser-entityparams)](applyconj[]editedretracted)))

(defnedited-fields[idparams]"Returns a map of non-empty values to be used as a Datomic entity."(into{:db/idid}(filtersecond{:user/firstname(:firstnameparams):user/lastname(:lastnameparams)...})))

And retracted-fields.

(defn-has-empty-value?[[kv]](empty?(strv)))(defn-keys-for-empty-vals"Get a list of keys from a map which have empty values."[m](keys(filterhas-empty-value?m)))(defn-retract-statement"Only build a retract statement for existing values."[old-entityk](let[v(getold-entityk)](whenv[:db/retract(:db/idold-entity)kv])))(defnretracted-fields"Get all retractions from an existing entity."[old-entitynew-entity](filtervidentity(map(partialretract-statementold-entity)(keys-for-empty-valsnew-entity))))