Constraints
Explains how Datomic handles SQL-like constraints (not null, unique, foreign key) and introduces custom attribute predicates and entity specs.
What this file does
Explains how Datomic handles SQL-like constraints (not null, unique, foreign key) and introduces custom attribute predicates and entity specs.
When to use it
- You are moving from SQL to Datomic and need to understand constraint equivalents
- You want to enforce data integrity rules like required fields or value ranges in Datomic
- You need to implement custom validation logic for attributes or entities in Datomic
- You are designing a Datomic schema and need guidance on unique and foreign key patterns
Assumes this stack
Constraints
In Day25, we discussed enumerated types (enums). To some extent, enums are also a type of constraint—they limit the values that can be assigned to a specific field to a predefined set.
When it comes to SQL databases, the most commonly used constraints are the following three:
- Not Null Constraint (not null)
- Unique Constraint (unique)
- Foreign Key (foreign key)
In my experience, for simple use cases, the above three are sufficient to ensure a reasonable degree of system correctness. Next, let’s explore how these common applications correspond to practices in Datomic.
Common Applications
Primary Key
In SQL, setting a primary key for a specific column implies two constraints: the value must not be null, and it must be unique.
However, in Datomic, this issue doesn’t arise because the primary key is the entity ID. The entity ID is unique across the entire database, and every entity is guaranteed to have one. Thus, it inherently satisfies both properties.
Not Null
Datomic does not have a corresponding syntax for the "not null" constraint because Datomic does not allow you to write a NULL value for any attribute. Therefore, such syntax is unnecessary.
How, then, can we represent that a specific entity lacks a particular attribute?
It’s simple—just don’t write it into the database. For example, in the transaction data (tx-data) below, we insert two items, each with two attributes:
[{:db/id item-1-id
:line-item/product chocolate
:line-item/quantity 1}
{:db/id item-2-id
:line-item/product whisky
:line-item/quantity 2}]
If we want item-3 to lack the :line-item/quantity attribute, we can write it as follows:
[{:db/id item-3-id
:line-item/product happy}]
Notice the advantage of this column schema: it provides clearer semantics.
Unique
Refer to the example below. In SQL, a unique constraint can be applied to a column. In Datomic, you only need to add :db/unique :db.unique/identity to the corresponding attribute:
{:db/ident :user/uuid,
:db/valueType :db.type/uuid,
:db/doc "Unique user identifier",
:db/cardinality :db.cardinality/one,
:db/unique :db.unique/identity}
Foreign Key
Datomic does not provide concise syntax to express foreign key semantics.
While foreign keys help ensure data consistency, they also introduce additional development overhead, especially during testing. Often, when creating simple integration tests, you might only want to generate test data for one table. However, with foreign keys, you might end up needing data for three tables instead.
As such, I believe Datomic deliberately avoids concise syntax for foreign key constraints because it does not encourage the traditional SQL semantic of foreign key.
Advanced Applications
Regarding foreign keys mentioned in the common applications section, Datomic does not provide concise syntax for them. This statement is only half the story. With custom assertion functions, there is virtually no complex constraint that cannot be expressed.
As demonstrated in Day18, if complex aggregation queries are hard to express, custom aggregation functions can be used, enhancing semantic clarity. The same applies to constraints in Datomic. It provides two types of custom constraints: Attribute Predicates for strengthening attribute semantics and Entity Specs for imposing various restrictions on entities.
Here’s a terminology clarification: in the context of Clojure/Datomic, "constraints" and "specs" are nearly synonymous and are often used interchangeably.
Attribute Predicates
Sometimes, we might declare an attribute’s data type as a string, but in practice, it always stores email addresses, which follow a specific format. In such cases, we can use attribute predicates to further enhance semantics.
Below is an example of checking that the string length for a name must be between 3 and 15 characters:
- Define a custom function
'datomic.samples.attr-preds/user-name?:
(ns datomic.samples.attr-preds)
(defn user-name?
[s]
(<= 3 (count s) 15))
- Assign the custom function to
:db.attr/predsto set up the attribute predicate:
{:db/ident :user/name,
:db/valueType :db.type/string,
:db/cardinality :db.cardinality/one,
:db.attr/preds 'datomic.samples.attr-preds/user-name?}
Entity Specs
Attribute predicates strengthen individual attribute constraints. But what if the constraints belong to the entire entity? For instance, certain entities must contain specific attributes, or relationships exist between entities.
For entity-wide constraints, Datomic provides Entity Specs syntax.
First, note that Entity Specs themselves are entities. Throughout this series, we’ve encountered many cases where Datomic uses entities to express semantics, including:
- Business domain data (e.g., people, products)
- Transactions
- Attributes
- Enumerated types
- Entity Specs
Required Fields
Here’s an example of using Entity Specs to enforce required fields in two steps:
- Define an Entity Spec:
Below, we define an Entity Spec called :user/validate, which requires :user/name and :user/email:
{:db/ident :user/validate
:db.entity/attrs [:user/name :user/email]}
- Use
:db/ensurein transaction data to trigger checks:
In the transaction data below, the :db/ensure virtual attribute ensures that Datomic checks whether the entity being written satisfies the corresponding Entity Spec:
{:user/name "John Doe"
:db/ensure :user/validate}
Relationships Between Data
Relationships between data require invoking functions within Entity Specs, i.e., Entity Predicates. Here’s an example consisting of three steps:
- Define a custom function
datomic.samples.entity-preds/scores-are-ordered?:
(ns datomic.samples.entity-preds
(:require [datomic.api :as d]))
(defn scores-are-ordered?
[db eid]
(let [m (d/pull db [:score/low :score/high] eid)]
(<= (:score/low m) (:score/high m))))
- Assign the custom function to
:db.entity/predsto set up the Entity Predicate:
{:db/ident :score/guard
:db.entity/attrs [:score/low :score/high] ;; required attributes
:db.entity/preds 'datomic.samples.entity-preds/scores-are-ordered?} ;; entity predicate
- Use
:db/ensurein transaction data to trigger checks:
The :db/ensure below triggers the :score/guard check when this transaction data is written:
{:score/low 100
:score/high 20
:db/ensure :score/guard}
References
What's inside
3 common constraint sections, 2 advanced constraint sections, 6 code examples, 1 reference link
Change this for your project
- Replace
datomic.samples.attr-preds/user-name?with your own namespace and function - Replace
datomic.samples.entity-preds/scores-are-ordered?with your own namespace and function - Replace
:user/validate,:user/name,:user/emailwith your own entity spec and attribute names - Replace
:score/guard,:score/low,:score/highwith your own entity spec and attribute names
Where it goes
Keep it in your repository where the agent or team that needs it will read it.
Worth borrowing
- Using
:db/unique:db.unique/identityto enforce uniqueness on an attribute - Using
:db.attr/predsto attach a custom validation function to an attribute - Using
:db.entity/predsand:db/ensureto enforce entity-level constraints
Related Documents
DunApp PWA - Project Constraints
Defines 14 hard constraints for a Hungarian PWA project, banning Netlify deployment and enforcing local-only testing, Supabase backend, and zero-cost development.
Constraints
Defines a three-tier priority system for design decisions, with conflict resolution examples to guide trade-offs.
Version Constraints Guide
Teaches Composer version constraint syntax for WordPress plugins and themes using a custom shell script wrapper.
Specifying version constraints
Explains how to pin Terraform CLI, provider, and Ansible versions for IBM Cloud Schematics workspaces and actions.