datalevin 2026-07-01

Does datalevin support entity predicates for required attrs and custom fns?

You can use a transaction function to do that.

But you can't e.g. automatically enforce required attrs

why not? a transaction function can validate whatever conditions

Sure, but you'd have to remember to use that tx-fn everywhere, and you'd have to wrap your tx-data in it, which can introduce composition issues. It sounds like that's not a supported feature, which is fine. I just was curious whether it was there or not

it it important? it's simple to add

I am looking at the entity predicate feature, it's a lot of boilerplate, it's not automatic, you have to use :db/ensure, so really, I don't see the point

Up to you. I don't need it for my anticipated usecase, but it might be valuable to people using datalevin as a primary datastore and want to be able to enforce that their data is well-formed at the storage level, especially if they're used to doing that in sql dbs (which I think a lot of ppl are)

now I have seen the feature, I don't think it is worthwhile, it's not automatic. You have to remember to use :db/ensure. Basically, you are not gaining anything for the added requirement.

I use :db/ensure a lot in datomic at work, I'd be happy to chat about my experience with it (good and bad) sometime if you're interested

if you have to remember to use :db/ensure, why not just call a transaction function, I don't really see the point

You do gain some composability over tx-fns, since :db/ensure can be added to existing tx-data rather than having to wrap it. Once you wrap tx-data it becomes opaque

it's a lot of ceremonies for something a txn function can do.

In an app where you tend to compose tx-data out of a bunch of different sources/fns, sometimes conditionally, that composition issue can become significant

it's the same thing. you can compose txn fn calls. They are all just data.

It's hard to inspect tx-data that contains tx-fn calls. We also tend to work in map-shaped tx-data when possible for the same reason

there's no difference between [:db.fn/call ...] and [:db/ensure ...]

The difference is with maps: I can conj :db/ensure into an existing map that might go through further processing before it's transacted, if I wrap that map in a tx-fn I can't inspect it anymore

don't know what you are talking about, you will have to show code

I do see the point of attribute predicates though, as it is automatic. If it's not, then there's no ergonomic gain

Imagine I have a codepath that creates payment destinations, called "accounts". Accounts come in 2 flavors: bank accounts and venmo. All accounts have an owner and an identity, but then they might have some other info that relates to which payment method they support. To make sure accounts are created with constent shapes, I have a fn for each account type that, given some input data, returns a transactable map for that account. Each of those type-specific fns calls a base account fn that adds the common attrs.

(defn ->bank-account [input}
  (assoc (base-account input)
         {:routing-number (...)
          ...}))

(defn ->venmo-account [input]
  (assoc (base-account input)
         {:venmo-handle (...)
          ...}))

(defn ->base-account [input]
  {:created-at (...)
   :owner (...)
   ...})
If I want all accounts to be ensured to be valid, I'd probably want to add :db/ensure to ->base-account. If I were to wrap the return value of ->base-account in a tx-fn, then I couldn't assoc to it in the type-specific fns. At one of my contracts, we find ourselves building up tx-data programmatically in ways that require inspecting it as you go very frequently, so you can imagine that this is a small example of a much larger pattern.

FWIW, I also think that :db/ensure being opt-in is a bit of a misfeature. I wish I could enable a default predicate for entities, though that runs counter to datomic's model somewhat of having entites by loose collections.

Yes, transaction function only checks your final function, but you can still compose functions. e.g.

(def account (->bank-account input))

  (d/transact! conn
    [[:db.fn/call create-account account]]) 
Considering the need to introduce extra syntax, and still need to remember to call :db/ensure, I would suggest people to go transaction function route for entity predicates, while I will add attribute predicate feature.

because schema is about attributes, there's really no easy way to hang an entity predicates. Hence the ceremonies. I would rather reduce the API surface.

I will add attribute predicates. Thanks for mentioning it.

I will add in the book examples of using transaction function to do entity attributes validation.

Oh yeah attribute predicates are applied automatically and would be a good addition

Thank you for the example. I will show in the book exactly the same example using transaction functions.

Basically, if your functions are returning maps, you can compose them just as easily.

In datomic, db/ensure is unique in that it is a post-condition after the entire tx-data is expanded to datoms. Tx fns can only see, thus enforce pre-conditions, so other tx data in the transaction may violate the intended invariant

That is why it’s not just sugar for tx fns

That's a good point. I can actually add a :db/ensure for post condition. It's new functionality.

What would be a good example use case for this though?

We have an entity predicate for each logical entity in our system to enforce invariants about it, like that required attributes are present, or that an account with a routing number also has an account number and vice versa. Sometimes you even have multiple entity predicates for an entity if it goes through multiple “stages”. The datomic docs cover this well and have decent examples iirc

Great! Will do. We will add a :db/ensure form in txn. Thank you for the input.