<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>tag:blog.danieljanus.pl,2019:category:datahike</id>
  <title>Daniel Janus – Datahike</title>
  <link href="http://blog.danieljanus.pl/category/datahike/"/>
  <updated>2026-07-24T00:00:00Z</updated>
  <author>
    <name>Daniel Janus</name>
    <uri>http://danieljanus.pl</uri>
    <email>dj@danieljanus.pl</email>
  </author>
  <entry>
    <id>tag:blog.danieljanus.pl,2026-07-24:post:datomic-map-values</id>
    <title>Things I wish Datomic had: Map values</title>
    <link href="http://blog.danieljanus.pl/datomic-map-values/"/>
    <updated>2026-07-24T00:00:00Z</updated>
    <content type="html">&lt;div&gt;&lt;h2 id="whence-this-post"&gt;Whence this post&lt;/h2&gt;&lt;p&gt;&lt;a href="https://openjdk.org/jeps/401"&gt;JEP 401&lt;/a&gt; – a proposal to add value classes to Java – has been crystallizing for almost six years now, but I only learned about it this morning. I skimmed it, nodding along to myself as I thought “gee, I can’t imagine going back to Java” and “wonder how this might make Clojure more performant?”. But then I thought about some Datomic (actually, Datahike) data remodelling that I’d been working on recently, and I realized that the two areas are connected.&lt;/p&gt;&lt;p&gt;So here’s a braindump, in an effort to clear up my mental image of all this.&lt;/p&gt;&lt;h2 id="a-simple-example"&gt;A simple example&lt;/h2&gt;&lt;p&gt;Consider this Clojure value:&lt;/p&gt;&lt;pre&gt;&lt;code class="hljs clojure"&gt;(&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;prog&lt;/span&gt;
  {&lt;span class="hljs-symbol"&gt;:program/name&lt;/span&gt;    &lt;span class="hljs-string"&gt;&amp;quot;Apache Maven&amp;quot;&lt;/span&gt;
   &lt;span class="hljs-symbol"&gt;:program/url&lt;/span&gt;     &lt;span class="hljs-string"&gt;&amp;quot;https://maven.apache.org&amp;quot;&lt;/span&gt;
   &lt;span class="hljs-symbol"&gt;:program/version&lt;/span&gt; {&lt;span class="hljs-symbol"&gt;:version/major&lt;/span&gt; &lt;span class="hljs-number"&gt;3&lt;/span&gt;
                     &lt;span class="hljs-symbol"&gt;:version/minor&lt;/span&gt; &lt;span class="hljs-number"&gt;9&lt;/span&gt;
                     &lt;span class="hljs-symbol"&gt;:version/patch&lt;/span&gt; &lt;span class="hljs-number"&gt;16&lt;/span&gt;}})
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you’re like me, you’ll have a warm, comfortable feeling in your heart looking at this. Plain data at rest. Accessible, transformable. What’s not to love?&lt;/p&gt;&lt;p&gt;Well, now try storing it in Datomic. Easy! Let’s first define a schema:&lt;/p&gt;&lt;pre&gt;&lt;code class="hljs clojure"&gt;(&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;schema&lt;/span&gt;
  [{&lt;span class="hljs-symbol"&gt;:db/ident&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:program/name&lt;/span&gt;
    &lt;span class="hljs-symbol"&gt;:db/valueType&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:db.type/string&lt;/span&gt;
    &lt;span class="hljs-symbol"&gt;:db/unique&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:db.unique/identity&lt;/span&gt;
    &lt;span class="hljs-symbol"&gt;:db/cardinality&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:db.cardinality/one&lt;/span&gt;}
   {&lt;span class="hljs-symbol"&gt;:db/ident&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:program/url&lt;/span&gt;
    &lt;span class="hljs-symbol"&gt;:db/valueType&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:db.type/uri&lt;/span&gt;
    &lt;span class="hljs-symbol"&gt;:db/cardinality&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:db.cardinality/one&lt;/span&gt;}
   {&lt;span class="hljs-symbol"&gt;:db/ident&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:version/major&lt;/span&gt;
    &lt;span class="hljs-symbol"&gt;:db/valueType&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:db.type/long&lt;/span&gt;
    &lt;span class="hljs-symbol"&gt;:db/cardinality&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:db.cardinality/one&lt;/span&gt;}
   {&lt;span class="hljs-symbol"&gt;:db/ident&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:version/minor&lt;/span&gt;
    &lt;span class="hljs-symbol"&gt;:db/valueType&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:db.type/long&lt;/span&gt;
    &lt;span class="hljs-symbol"&gt;:db/cardinality&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:db.cardinality/one&lt;/span&gt;}
   {&lt;span class="hljs-symbol"&gt;:db/ident&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:version/patch&lt;/span&gt;
    &lt;span class="hljs-symbol"&gt;:db/valueType&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:db.type/long&lt;/span&gt;
    &lt;span class="hljs-symbol"&gt;:db/cardinality&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:db.cardinality/one&lt;/span&gt;}
   {&lt;span class="hljs-symbol"&gt;:db/ident&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:program/version&lt;/span&gt;
    &lt;span class="hljs-symbol"&gt;:db/valueType&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:db.type/ref&lt;/span&gt;
    &lt;span class="hljs-symbol"&gt;:db/isComponent&lt;/span&gt; &lt;span class="hljs-literal"&gt;true&lt;/span&gt;
    &lt;span class="hljs-symbol"&gt;:db/cardinality&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:db.cardinality/one&lt;/span&gt;}])
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And now we can transact it (I’ll assume we have a DB connection, &lt;code&gt;conn&lt;/code&gt;):&lt;/p&gt;&lt;pre&gt;&lt;code class="hljs clojure"&gt;@(&lt;span class="hljs-name"&gt;d/transact&lt;/span&gt; conn schema)
@(&lt;span class="hljs-name"&gt;d/transact&lt;/span&gt; conn [prog])
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Done. Now we can check what Maven’s version is:&lt;/p&gt;&lt;pre&gt;&lt;code class="hljs clojure"&gt;(&lt;span class="hljs-name"&gt;&lt;span class="hljs-built_in"&gt;let&lt;/span&gt;&lt;/span&gt; [db (&lt;span class="hljs-name"&gt;d/db&lt;/span&gt; conn)
      mvn (&lt;span class="hljs-name"&gt;d/entity&lt;/span&gt; db [&lt;span class="hljs-symbol"&gt;:program/name&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;Apache Maven&amp;quot;&lt;/span&gt;])]
  (&lt;span class="hljs-symbol"&gt;:program/version&lt;/span&gt; (&lt;span class="hljs-name"&gt;d/touch&lt;/span&gt; mvn)))
&lt;span class="hljs-comment"&gt;;=&amp;gt; {:db/id 17592186045419, :version/major 3, :version/minor 9, :version/patch 16}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It works!&lt;/p&gt;&lt;h2 id="meh"&gt;Meh&lt;/h2&gt;&lt;p&gt;But I’m not exactly happy about this.&lt;/p&gt;&lt;p&gt;What I’d really like to get is &lt;code&gt;#:version{:major 3, :minor 9, :patch 16}&lt;/code&gt;. But Datomic models maps as entities – focal points that bind attributes with values. That’s fine for the top-level &lt;code&gt;program&lt;/code&gt; map, but &lt;code&gt;version&lt;/code&gt; is not an entity! It’s just a value, like a number or a string. It has internal structure, but, conceptually, it’s just an atomic value, an element of the set of all possible &lt;code&gt;major.minor.patch&lt;/code&gt; version numbers.&lt;/p&gt;&lt;p&gt;Yet Datomic forces us to “reify” the version map as an entity. That means that it automatically gets a &lt;code&gt;:db/id&lt;/code&gt;. If a new version of Maven gets released, and we transact that fact:&lt;/p&gt;&lt;pre&gt;&lt;code class="hljs clojure"&gt;@(&lt;span class="hljs-name"&gt;d/transact&lt;/span&gt; conn
             [[[&lt;span class="hljs-symbol"&gt;:program/name&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;Apache Maven&amp;quot;&lt;/span&gt;]
               &lt;span class="hljs-symbol"&gt;:program/version&lt;/span&gt;
               #:version{&lt;span class="hljs-symbol"&gt;:major&lt;/span&gt; &lt;span class="hljs-number"&gt;3&lt;/span&gt;&lt;span class="hljs-punctuation"&gt;,&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:minor&lt;/span&gt; &lt;span class="hljs-number"&gt;9&lt;/span&gt;&lt;span class="hljs-punctuation"&gt;,&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:patch&lt;/span&gt; &lt;span class="hljs-number"&gt;17&lt;/span&gt;}]])
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;then Datomic will create a fresh artificial entity, give it the three version attributes, and associate it with the Maven entity. But the old one still exists in the DB! It’s “semi-orphaned” (detached from the rest of the object graph in the current state, but still reachable via history). If Maven for some reason goes back to 3.9.16 via a similar transaction, then we’ll get a &lt;em&gt;third&lt;/em&gt; entity that is a &lt;em&gt;duplicate&lt;/em&gt; of the first one.&lt;/p&gt;&lt;p&gt;Even worse, there’s nothing stopping us from changing attributes of that entity:&lt;/p&gt;&lt;pre&gt;&lt;code class="hljs clojure"&gt;(&lt;span class="hljs-name"&gt;&lt;span class="hljs-built_in"&gt;let&lt;/span&gt;&lt;/span&gt; [db (&lt;span class="hljs-name"&gt;d/db&lt;/span&gt; conn)
      mvn (&lt;span class="hljs-name"&gt;d/entity&lt;/span&gt; db [&lt;span class="hljs-symbol"&gt;:program/name&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;Apache Maven&amp;quot;&lt;/span&gt;])]
  @(&lt;span class="hljs-name"&gt;d/transact&lt;/span&gt; conn [[(&lt;span class="hljs-name"&gt;&lt;span class="hljs-built_in"&gt;-&amp;gt;&lt;/span&gt;&lt;/span&gt; mvn &lt;span class="hljs-symbol"&gt;:program/version&lt;/span&gt; &lt;span class="hljs-symbol"&gt;:db/id&lt;/span&gt;) &lt;span class="hljs-symbol"&gt;:version/major&lt;/span&gt; &lt;span class="hljs-number"&gt;4&lt;/span&gt;]]))
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now the identity of version hasn’t changed, but Maven is at 4.9.17, and every other entity that happened to be referencing the artificial version one is at 4.9.17 too! Clearly, this kind of thing should be disallowed.&lt;/p&gt;&lt;p&gt;Also note that I had to say &lt;code&gt;:db/isComponent true&lt;/code&gt; in the schema for the transaction to have succeeded at all. &lt;code&gt;isComponent&lt;/code&gt; means that the child entity only makes sense in the context of parent; otherwise, I’d have to lift the version map to the top-level of the transaction, give it a temporary id, and use that id to refer to it in the program map.&lt;/p&gt;&lt;h2 id="two-kinds-of-maps"&gt;Two kinds of maps&lt;/h2&gt;&lt;p&gt;At this point, I realize there are two kinds of maps: those that describe snapshots of state of some stateful entity at some point in time (like &lt;code&gt;program&lt;/code&gt;), and those that are merely juxtapositions of named values (like &lt;code&gt;version&lt;/code&gt;). For want of better names, for now on I’ll call these “snapshot maps” and “plain maps”, respectively.&lt;/p&gt;&lt;p&gt;Quoting from &lt;a href="https://clojure.org/about/state"&gt;Clojure’s Approach to Identity and State&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;We need to move away from a notion of state as "the content of this memory block" to one of "the value currently associated with this identity". Thus an identity can be in different states at different times, but &lt;em&gt;the state itself doesn’t change&lt;/em&gt;. That is, an identity is not a state, an identity &lt;em&gt;&lt;strong&gt;has&lt;/strong&gt;&lt;/em&gt; a state. Exactly one state at any point in time. And that state is a true value, i.e. it never changes.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;With this in mind, we could reformulate the distinction as follows: “snapshot maps” are the state of &lt;em&gt;some&lt;/em&gt; identity, while “plain maps” are not.&lt;/p&gt;&lt;p&gt;In Clojure, the two kinds of maps look and work exactly the same, but as we saw, they are conceptually very different. It is typically obvious which kind a given map belongs to; e.g. snapshot maps typically have a &lt;code&gt;:id&lt;/code&gt; field that holds an integer or a UUID. But sometimes the exact same map can be viewed as either (is &lt;code&gt;{:x 1 :y 2}&lt;/code&gt; just a 2D point – a plain map – or a snapshot of a point moving around – a snapshot map?) It’s not just maps either; we could make the same distinction for vectors, or indeed scalar values, but maps are where it’s most commonly encountered.&lt;/p&gt;&lt;p&gt;I find the distinction philosophically interesting. For example, could we envision a variant of Clojure that makes it explicit? Or discriminate between them based on the value’s metadata? Should &lt;code&gt;deref&lt;/code&gt; automatically set that metadata? Should operations on the map preserve that metadata?&lt;/p&gt;&lt;p&gt;Besides semantic version numbers, here are some more examples of plain maps that appear frequently:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;span&gt;Temporal values. These are typically disguised as instances of &lt;code&gt;java.util.Date&lt;/code&gt; or &lt;code&gt;java.time.*&lt;/code&gt; classes, but are conceptually immutable collections of fields. For example, &lt;code&gt;java.time.Instant&lt;/code&gt; values look like &lt;code&gt;{:epochSecond long, :nano int}&lt;/code&gt;.&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span&gt;Prices, consisting of an amount and a currency. Back when &lt;a href="https://www.iamfy.co/"&gt;Fy!&lt;/a&gt; used Datomic, we modelled prices as “artificial entities” as described in this post, leading to a massive proliferation of duplicate/orphaned price entities accruing in everyday operation.&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id="what-else-could-we-do?"&gt;What else could we do?&lt;/h2&gt;&lt;p&gt;Going back to Datomic: if we want to avoid artificial entities, what alternatives do we have?&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Store them as strings instead, like &lt;code&gt;"3.9.16"&lt;/code&gt;. Strings are plain values and they map cleanly onto how we typically denote versions. This may be the right solution if our app also presents versions this way and doesn’t do any fancy processing of it. A potential problem is that strings don’t sort correctly as versions: &lt;code&gt;"11.0.2"&lt;/code&gt; is lexicographically smaller than &lt;code&gt;"2.2.1"&lt;/code&gt;, so the values will appear in an incoherent order in the AVET index. If we want to extract programs with versions in a certain range, the index can’t help us – we’ll need to do a full scan of the DB, then parse and sort manually.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Store them as &lt;a href="https://docs.datomic.com/schema/schema-reference.html#tuples"&gt;tuples&lt;/a&gt; instead, like &lt;code&gt;[3 9 16]&lt;/code&gt;. Datomic supports short vectors as values that don’t have an identity. In our case, we can represent versions numbers as 3-tuples of longs (either homo- or heterogeneous ones will do), &lt;code&gt;[major minor patch]&lt;/code&gt;. This gives us the correct sorting. The downside is that the rest of our app probably doesn’t think about versions this way, so we need to translate the “domain” values that we use elsewhere to tuples before transacting, and translate it back in the data access layer.&lt;br&gt;&lt;br&gt;This leads to increased verbosity (one of the nice things about Datomic is that, most of the time, it lets you get away without having a data access layer at all, as you can use the entities returned by &lt;code&gt;d/entity&lt;/code&gt; directly in your domain logic code). Still, I think it’s &lt;em&gt;the&lt;/em&gt; right thing to do.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Flatten the data: don’t have the &lt;code&gt;:program/version&lt;/code&gt; attribute at all, and instead associate the &lt;code&gt;:version/*&lt;/code&gt; attributes directly with the program entity. This may make sense in some cases, but loses grouping, which can make it harder to programmatically process such data. Also, what if the program could have many versions?&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Use Datahike instead of Datomic, and use its &lt;a href="https://github.com/replikativ/datahike/blob/main/doc/unstructured.md"&gt;unstructured input&lt;/a&gt; feature in the “content identity” mode. This still creates artificial entities, but avoids duplication because it automatically infers ids from the map content, so two maps with the same content will refer to the same entity. However, it still has the “accidental mutability” problem with the artificial entity. You have to either opt in to content identity for all sub-maps of the map being transacted, or opt out of it en masse. Plus, it just feels hacky.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id="in-an-ideal-world…"&gt;In an ideal world…&lt;/h2&gt;&lt;p&gt;…I’d like to be able to just define new types in Datomic. Just like there’s built-in supports for &lt;code&gt;java.util.Date&lt;/code&gt;s (as &lt;code&gt;:db.type/inst&lt;/code&gt;) or &lt;code&gt;java.net.URI&lt;/code&gt;s (as &lt;code&gt;:db.type/uri&lt;/code&gt;), I’d like to somehow tell Datomic to “support instances of  &lt;code&gt;java.time.LocalDate&lt;/code&gt; as &lt;code&gt;:db.type/date&lt;/code&gt;”. Or, “let &lt;code&gt;:db.type/semver&lt;/code&gt; be a map mapping &lt;code&gt;:version/major&lt;/code&gt;, &lt;code&gt;:version/minor&lt;/code&gt;, and &lt;code&gt;:version/patch&lt;/code&gt; to longs”.&lt;/p&gt;&lt;p&gt;How such an API could look like is open to discussion: I don’t have concrete ideas here. Whatever the design, though, I think it’d be a win.&lt;/p&gt;&lt;/div&gt;</content>
  </entry>
</feed>
