<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://finnvolkel.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://finnvolkel.com/" rel="alternate" type="text/html" /><updated>2026-06-07T12:30:45+00:00</updated><id>https://finnvolkel.com/feed.xml</id><title type="html">Finn Völkel</title><author><name>Finn Völkel</name></author><entry><title type="html">Triplox Log 1 - Introduction</title><link href="https://finnvolkel.com/triplox-log-1-introduction" rel="alternate" type="text/html" title="Triplox Log 1 - Introduction" /><published>2026-04-28T00:00:00+00:00</published><updated>2026-04-28T00:00:00+00:00</updated><id>https://finnvolkel.com/triplox-log-1-introduction</id><content type="html" xml:base="https://finnvolkel.com/triplox-log-1-introduction"><![CDATA[<p>I am working on a Datalog database engine à la <a href="https://www.datomic.com/">Datomic</a> on top of object storage. The system is called Triplox (a portemanteau of Triple and Blocks). In an attempt to become a better communicator I decided to start a little log to explain some concepts in Triplox.</p>

<p>The backbone of the engine is <a href="https://github.com/slatedb/slatedb/">SlateDB</a>, a key-value store on top of object-storage. Think of SlateDB as <a href="https://github.com/facebook/rocksdb">RocksDB</a> on top of object-storage. Datomic is the main inspiration and the data model, transaction semantics and query API closely follow Datomic.</p>

<p>By making object storage the single source of truth, you get separation of storage and compute. SlateDB has a single writer and many readers architecture and that naturally translates to Triplox. In that sense it’s similar to Datomic. Triplox sits more in the traditional client/server camp compared to Datomic where the <a href="https://docs.datomic.com/operation/peer-server.html">peer library</a> gets embedded into the application code.</p>

<p>The goals of Triplox are roughly the following (in no particular order):</p>
<ul>
  <li>Object storage first. In it’s final version Triplox should simply need a single S3 bucket for deployment. You will see further down that this is currently not really the case.</li>
  <li>The Datomic Data model and API as main inspiration. Datomic is awesome. Let’s bring it to S3 and make it easily scalable.</li>
  <li>A Client/Server architecture. I hope that this will open the door to ecosystems outside of the JVM (where Datomic has had it’s main success).</li>
  <li>Incremental queries à la <a href="https://arxiv.org/abs/2203.16684">DBSP</a>. You should be able to dynamically subscribe and detach from incremental Datalog queries. This is different to <a href="https://github.com/feldera/feldera">Feldera</a> which compiles a new binary for every query. So incremental queries should be a lot lighter in Triplox. This is the most experimental part of Triplox and will need quite a bit of engineering effort to get right, make fast, and fully support of all features of Datalog (recursive rules being the most tricky part). The idea is to hook into SlateDB’s <a href="https://en.wikipedia.org/wiki/Change_data_capture">CDC</a> and produce new deltas for every WAL entry that comes through.</li>
</ul>

<p>SlateDB is built with OLTP access patterns in mind and this translates directly to Triplox. If you plan to do giant OLAP aggregations on top of Triplox you might be bettter served by a different system. This doesn’t mean Triplox doesn’t support aggregates, it will just never beat something like <a href="https://github.com/duckdb/duckdb">DuckDB</a> on these types of queries.</p>

<p>The architecture of Triplox for a 3 node setup then would roughly look as follows (subject to change):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                   ┌─────────────────────────────────────────────────────────────┐
                   │                   Object Storage (S3)                       │
                   │                                                             │
                   │  ┌─────────────┐      ┌─────────────┐      ┌─────────────┐  │
                   │  │   SlateDB   │      │   SlateDB   │      │   SlateDB   │  │
                   │  │  (Writer)   │      │  (Reader 1) │      │  (Reader 2) │  │
                   │  └──────┬──────┘      └──────┬──────┘      └──────┬──────┘  │
                   │         │                    │                    │         │
                   └─────────┼────────────────────┼────────────────────┼─────────┘
                             │                    │                    │
     Queries/Indices         ▲ read/write         ▼ read               ▼ read
                             │                    │                    │
        ┌────────────────────┴────────┐  ┌────────┴────────┐  ┌────────┴───────┐
        │         Writer Node         │  │  Reader Node 1  │  │  Reader Node 2 │
        │                             │  │                 │  │                │
        │      ┌──────────────┐       │  │                 │  │                │
  ┌─────┼────▶│   Indexer    │       │  │                 │  │                │
  │     │      └──────────────┘       │  │                 │  │                │
  │     │                             │  │                 │  │                │
  │     └─────────────┬───────────────┘  └─────────────────┘  └────────────────┘
  │                   │
  │  Transactions     │ write
  │                   ▼
  │     ┌──────────────────────────────────────────────────────────────────────┐
  │     │                                                                      │
  │     │                 Log (Kafka, S2, WAL3, etc.)                          │
  │     │                                                                      │
  │     │    ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐                 │
  └─────┼────┤ tx0 │ tx1 │ tx2 │ tx3 │ tx4 │ tx5 │ tx6 │ ... │                 │
   read │    └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘                 │
        │                                                                      │
        └──────────────────────────────────────────────────────────────────────┘
</code></pre></div></div>
<p>The main wrinkle here is currently the log. It adds extra complexity I would prefer to avoid. Using something like <a href="https://github.com/automq/automq">AutoMQ</a>, which is Kafka backed by S3, would create another service in the architecture.  Some kind of log component to which the writer node just appends data would be preferable.   I am not using SlateDB’s <a href="https://en.wikipedia.org/wiki/Multiversion_concurrency_control">MVCC</a> because Triplox needs to control the <a href="https://en.wikipedia.org/wiki/Total_order">total order</a> of transactions before they hit the indexes, rather than having that order determined internally by SlateDB.
Maybe something like <a href="https://www.trychroma.com/engineering/wal3">wal3</a> would fit the bill, but it is currently not available as standalone dependency. We have also discussed creating a standalone slatedb-wal, extracting the wal component of SlateDB into a standalone dependency and simply use it as a log. So far this seems to be the best option to me, but I am happy to hear other ideas.</p>

<p>The following sections are likely familiar to Datomic users and can in this case be skipped.</p>
<h2 id="data-model">Data model</h2>
<p>Triplox is an Entity-Attribute-Value (EAV) triple store. The database is made up of a set of triples called Datoms. Each Datom declares that some entity (for example, a person)  has a certain attribute (like a name) of a particular value (like “Ada Lovelace”). <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> An entity might have many attributes. A <a href="https://docs.datomic.com/schema/schema-reference.html">schema</a> defines the valid types and cardinality of attributes. This schema is also stored as triples. The system is self-referential and the only way data is stored in Triplox (also at the meta level) is in form of triples. Examples speak a thousand words. Consider the following person entity.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:person/first-name</span><span class="w"> </span><span class="s">"Ada"</span><span class="w">
 </span><span class="no">:person/last-name</span><span class="w"> </span><span class="s">"Lovelace"</span><span class="w">
 </span><span class="no">:person/sex</span><span class="w"> </span><span class="no">:female</span><span class="w">
 </span><span class="no">:person/profession</span><span class="w"> </span><span class="s">"programmer"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>It will get expanded into 4 triples (aka Datoms)</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mi">123</span><span class="w"> </span><span class="no">:person/first-name</span><span class="w"> </span><span class="s">"Ada"</span><span class="p">]</span><span class="w">
</span><span class="p">[</span><span class="mi">123</span><span class="w"> </span><span class="no">:person/last-name</span><span class="w"> </span><span class="s">"Lovelace"</span><span class="p">]</span><span class="w">
</span><span class="p">[</span><span class="mi">123</span><span class="w"> </span><span class="no">:person/sex</span><span class="w"> </span><span class="no">:female</span><span class="p">]</span><span class="w">
</span><span class="p">[</span><span class="mi">123</span><span class="w"> </span><span class="no">:person/profession</span><span class="w"> </span><span class="s">"programmer"</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>
<p>The 123 is what is called an entity id. A unique ID identifying an entity. These triples are stored in indexes <code class="language-plaintext highlighter-rouge">EAV</code>,<code class="language-plaintext highlighter-rouge">AVE</code>, <code class="language-plaintext highlighter-rouge">AEV</code> and <code class="language-plaintext highlighter-rouge">VAE</code> (the order of the initials means the order in which the triple is stored in each index). So <code class="language-plaintext highlighter-rouge">[123 :person/first-name "Ada"]</code> is stored as such in the EAV index and stored as <code class="language-plaintext highlighter-rouge">[:person/first-name "Ada" 123]</code> in the AVE index and so forth.</p>

<p>The four indexes EAV, AVE, AEV and VAE are called the covering indexes. The reason the same data is stored 4 times are access patterns and joins.</p>

<ul>
  <li>The EAV index lets you quickly find all attributes plus their values. What do we know about Ada Lovelace?</li>
  <li>With the AVE index you can lookup entities that match a certain AV pair. Who is a professional programmer? One can also efficiently find entities for range queries. Which people are between the age <code class="language-plaintext highlighter-rouge">30</code> and <code class="language-plaintext highlighter-rouge">40</code>?</li>
  <li>The AEV index gives you “columnar style” access to an attribute. When you have a pattern like <code class="language-plaintext highlighter-rouge">[?e :person/age ?v]</code> (<code class="language-plaintext highlighter-rouge">?e</code> and <code class="language-plaintext highlighter-rouge">?v</code> being free variables) it’s often the case that <code class="language-plaintext highlighter-rouge">?v</code> doesn’t get constrained further, but <code class="language-plaintext highlighter-rouge">?e</code> will be (because of other triple patterns) and so it’s essential to get the entity id in sorted order for further joining. In the context of <a href="wcoj-datalog-and-genericjoin">WCOJ</a> it’s also essential to have both AEV and AVE because <code class="language-plaintext highlighter-rouge">?e</code> and <code class="language-plaintext highlighter-rouge">?v</code> may come first in different join orders.</li>
  <li>The schema allows for value types of <code class="language-plaintext highlighter-rouge">:db/ref</code>  which is a reference to another entity. For example <code class="language-plaintext highlighter-rouge">[?alice :person/follows ?bob]</code>. The <code class="language-plaintext highlighter-rouge">:person/follows</code> attribute points to another entity. The VAE index is only populated for reference attributes. It allows you do to do certain graph traversal navigation in reverse order. “Who is following Bob?” for the example above.</li>
</ul>

<p>I have glossed over some aspects of a Datom. In reality a Datom is actually a 5-tuple of <code class="language-plaintext highlighter-rouge">[entity-id attribute value txn added?]</code>. In many contexts we are still using the term triple to refer to a Datom as the EAV part is the important part for queries. The<code class="language-plaintext highlighter-rouge">txn</code> is the entity id of the transaction this particular triple was added to Triplox and <code class="language-plaintext highlighter-rouge">added?</code> identifies if the triple was added or retracted. Triplox like Datomic is an immutable system of record. Every addition and retraction is stored. You can always go back to a previous version of a database an run a query.</p>

<p>So when adding an entity like the above to Triplox</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:person/first-name</span><span class="w"> </span><span class="s">"Ada"</span><span class="w">
 </span><span class="no">:person/last-name</span><span class="w"> </span><span class="s">"Lovelace"</span><span class="w">
 </span><span class="no">:person/sex</span><span class="w"> </span><span class="no">:female</span><span class="w">
 </span><span class="no">:person/profession</span><span class="w"> </span><span class="s">"programmer"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>you actually get the following expanded Datoms in Triplox</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mi">123</span><span class="w"> </span><span class="no">:person/first-name</span><span class="w"> </span><span class="s">"Ada"</span><span class="w"> </span><span class="mi">124</span><span class="w"> </span><span class="n">true</span><span class="p">]</span><span class="w">
</span><span class="p">[</span><span class="mi">123</span><span class="w"> </span><span class="no">:person/last-name</span><span class="w"> </span><span class="s">"Lovelace"</span><span class="w"> </span><span class="mi">124</span><span class="w"> </span><span class="n">true</span><span class="p">]</span><span class="w">
</span><span class="p">[</span><span class="mi">123</span><span class="w"> </span><span class="no">:person/sex</span><span class="w"> </span><span class="no">:female</span><span class="w"> </span><span class="mi">124</span><span class="w"> </span><span class="n">true</span><span class="p">]</span><span class="w">
</span><span class="p">[</span><span class="mi">123</span><span class="w"> </span><span class="no">:person/profession</span><span class="w"> </span><span class="s">"programmer"</span><span class="w"> </span><span class="mi">124</span><span class="w"> </span><span class="n">true</span><span class="p">]</span><span class="w">
</span><span class="p">[</span><span class="mi">124</span><span class="w"> </span><span class="no">:db/txId</span><span class="w"> </span><span class="mi">124</span><span class="w"> </span><span class="mi">124</span><span class="w"> </span><span class="n">true</span><span class="p">]</span><span class="w">
</span><span class="p">[</span><span class="mi">124</span><span class="w"> </span><span class="no">:db/Instant</span><span class="w"> </span><span class="o">#</span><span class="n">inst</span><span class="w"> </span><span class="s">"2026"</span><span class="w"> </span><span class="mi">124</span><span class="w"> </span><span class="n">true</span><span class="p">]</span><span class="w">
</span><span class="p">[</span><span class="mi">124</span><span class="w"> </span><span class="no">:db/txResult</span><span class="w"> </span><span class="no">:db.result/commited</span><span class="w"> </span><span class="mi">124</span><span class="w"> </span><span class="n">true</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>
<p>The first 4 Datoms are the same as above (plus the transaction and assertion parts), the latter 3 are transaction Datoms. So also the transaction history is represented as triples (I hope you slowly get it, it’s triples everywhere 😉). In case Ada Lovelace had a different profession before she became a programmer like carpenter,  you would also get a retraction like</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mi">123</span><span class="w"> </span><span class="no">:person/profession</span><span class="w"> </span><span class="s">"carpenter"</span><span class="w"> </span><span class="mi">124</span><span class="w"> </span><span class="n">false</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>The entity id allocation (123 and 124) is simplified. Triplox will also support partitions. Entity ids can be allocated in different partitions. Partitions are assigned through the higher bits of an entity id . This will give you index locality when joining data in the same partition. In the beginning we will only have 3 partitions: A <code class="language-plaintext highlighter-rouge">DB_PARTITION</code> holding entities related to the schema and other database related concepts, a <code class="language-plaintext highlighter-rouge">TX_PARTITION</code> for transaction entities and a <code class="language-plaintext highlighter-rouge">USER_PARTITION</code> holding most of the user data. In the future we will likely add an option to create user partitions and allow users to specify partition assignment (via a special attribute).</p>

<p>You might wonder why not store entities like rows as in most traditional DBMSs. As outlined above the covering indexes give you good options for many different access patterns. Another advantage is flexibility and granularity. In a traditional row based stores every column needs to get filled (or nulled) for every row. The entity-attribute model allows for very flexible entity types. An example are sparse types.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">;; A book</span><span class="w">
</span><span class="p">{</span><span class="no">:product/sku</span><span class="w"> </span><span class="s">"B-001"</span><span class="w"> 
 </span><span class="no">:product/price</span><span class="w"> </span><span class="mf">19.99</span><span class="w"> 
 </span><span class="no">:book/isbn</span><span class="w"> </span><span class="s">"978-0-13..."</span><span class="w"> 
 </span><span class="no">:book/author</span><span class="w"> </span><span class="s">"Shannon"</span><span class="p">}</span><span class="w">

</span><span class="c1">;; An apparel — same "type", different attributes</span><span class="w">
</span><span class="p">{</span><span class="no">:product/sku</span><span class="w">          </span><span class="s">"A-Hoodie-042"</span><span class="w">
 </span><span class="no">:product/price</span><span class="w">        </span><span class="mf">24.99</span><span class="w">
 </span><span class="no">:apparel/size</span><span class="w">         </span><span class="no">:size/m</span><span class="w">
 </span><span class="no">:apparel/color</span><span class="w">        </span><span class="no">:color/black</span><span class="w">
 </span><span class="no">:apparel/material</span><span class="w">     </span><span class="s">"100% organic cotton"</span><span class="p">}</span><span class="w"> 
</span></code></pre></div></div>
<p>A store might sells books, apparel and potentially all kinds of other stuff. In SQL you would solve this problem with <a href="https://en.wikipedia.org/wiki/Single_Table_Inheritance">Single Table Inheritance</a>, which often creates sparse table pathologies.</p>

<p>A second example are join tables from SQL. They model many-to-many relationships.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">students</span> <span class="p">(</span>
  <span class="n">id</span>   <span class="nb">INTEGER</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
  <span class="n">name</span> <span class="nb">TEXT</span>
<span class="p">);</span>

<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">courses</span> <span class="p">(</span>
  <span class="n">id</span>    <span class="nb">INTEGER</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
  <span class="n">title</span> <span class="nb">TEXT</span>
<span class="p">);</span>

<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">enrollments</span> <span class="p">(</span>
  <span class="n">student_id</span> <span class="nb">INTEGER</span> <span class="k">REFERENCES</span> <span class="n">students</span><span class="p">(</span><span class="n">id</span><span class="p">),</span>
  <span class="n">course_id</span>  <span class="nb">INTEGER</span> <span class="k">REFERENCES</span> <span class="n">courses</span><span class="p">(</span><span class="n">id</span><span class="p">),</span>
  <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="p">(</span><span class="n">student_id</span><span class="p">,</span> <span class="n">course_id</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>
<p>In the Datomic data model the pattern kind of resolves. You simply have the cardinality many attribute <code class="language-plaintext highlighter-rouge">:student/course</code> of type reference which maps a student to courses. The VAE index (V being a reference pointing to a course) let’s you navigate this relationship in reverse if you like to find all students for a particular course.</p>
<h2 id="query-language">Query language</h2>
<p>The main query language for Triplox is a variant of <a href="https://en.wikipedia.org/wiki/Datalog">Datalog</a>. Datalog is a logic-based query language inspired by <a href="https://en.wikipedia.org/wiki/Prolog">Prolog</a>. A Datalog program consists of a set of facts. These facts are the Datoms that sit in our covering indexes. Everything else is derived from these facts (modulo incoming parameters).</p>
<h3 id="triple-pattern">Triple pattern</h3>
<p>You match a certain pattern against these facts. Consider the pattern <code class="language-plaintext highlighter-rouge">[?e :person/age 42]</code> . <code class="language-plaintext highlighter-rouge">?e</code> is a free variable meaning it “joins” against any triple in the indices for which the latter two hold true. It would find us the entities of people with age 42. Most of the time you want to know more about entities.  For this aspect of Datalog has the concept of unification.
Consider the query <sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup></p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w">  </span><span class="p">[</span><span class="n">?e</span><span class="w"> </span><span class="n">?x</span><span class="p">]</span><span class="w">
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[[</span><span class="n">?e</span><span class="w"> </span><span class="no">:age</span><span class="w"> </span><span class="mi">42</span><span class="p">]</span><span class="w">
         </span><span class="p">[</span><span class="n">?e</span><span class="w"> </span><span class="no">:likes</span><span class="w"> </span><span class="n">?x</span><span class="p">]]}</span><span class="w">
</span></code></pre></div></div>
<p>The clauses in the <code class="language-plaintext highlighter-rouge">:where</code> specify the triples we are interested in. In this case people of age 42 and and what they like.  First we find people of age 42 and then the unification of <code class="language-plaintext highlighter-rouge">?e</code> happens. The <code class="language-plaintext highlighter-rouge">?e</code>  now gets unified with the second triple pattern where we are looking for things people like (if they like anything ;)) by unifying their likings with <code class="language-plaintext highlighter-rouge">?x</code>. I am simplifying how Triplox actually does variable joins under the hood (I have described the <a href="wcoj-datalog-and-genericjoin">join algorithm here</a>), but this a good conceptual start for understanding unification. The <code class="language-plaintext highlighter-rouge">find</code> part is purely about the projection of the join variables.  Unification is the most fundamental part of Datalog and everything else follows naturally.</p>
<h3 id="or">Or</h3>
<p>By default everything in the where clause is a conjuction (an <code class="language-plaintext highlighter-rouge">and</code>) of the facts that satisfy the triples. If you want to express disjunctions you need an <code class="language-plaintext highlighter-rouge">or</code> clause.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">?e</span><span class="p">]</span><span class="w"> 
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[[</span><span class="n">?e</span><span class="w"> </span><span class="no">:age</span><span class="w"> </span><span class="mi">42</span><span class="p">]</span><span class="w">
         </span><span class="p">(</span><span class="nb">or</span><span class="w"> </span><span class="p">[</span><span class="n">?e</span><span class="w"> </span><span class="no">:likes</span><span class="w"> </span><span class="s">"ice cream"</span><span class="p">]]</span><span class="w">
             </span><span class="p">[</span><span class="n">?e</span><span class="w"> </span><span class="no">:likes</span><span class="w"> </span><span class="s">"donuts"</span><span class="p">])]}</span><span class="w">

</span></code></pre></div></div>
<p>In this case, the outer unification can happen against any of the inner <code class="language-plaintext highlighter-rouge">or </code>branches. The above query will find us people who are 42 years old and like donuts or ice cream. A person who likes both ice cream and donuts will only appear once in the output.</p>
<h3 id="and">And</h3>
<p>In <code class="language-plaintext highlighter-rouge">or</code> clauses disjunction is the default. If you want to get back to conjunction you need to use <code class="language-plaintext highlighter-rouge">and</code> clause.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">?e</span><span class="p">]</span><span class="w"> 
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[[</span><span class="n">?e</span><span class="w"> </span><span class="no">:age</span><span class="w"> </span><span class="mi">42</span><span class="p">]</span><span class="w">
         </span><span class="p">(</span><span class="nb">or</span><span class="w"> </span><span class="p">[</span><span class="n">?e</span><span class="w"> </span><span class="no">:likes</span><span class="w"> </span><span class="s">"icecream"</span><span class="p">]]</span><span class="w">
             </span><span class="p">(</span><span class="nb">and</span><span class="w"> </span><span class="p">[</span><span class="n">?e</span><span class="w"> </span><span class="no">:profession</span><span class="w"> </span><span class="s">"programmer"</span><span class="p">]</span><span class="w">
                  </span><span class="p">[</span><span class="n">?e</span><span class="w"> </span><span class="no">:likes</span><span class="w"> </span><span class="s">"donuts"</span><span class="p">]))]}</span><span class="w">
</span></code></pre></div></div>
<p>The above query finds us people who are 42 years old and who like icecream or are professional programmers who like donuts.</p>
<h3 id="not">Not</h3>
<p>In case you want to exclude certain types of facts you need to use the <code class="language-plaintext highlighter-rouge">not</code> clause.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">?e</span><span class="p">]</span><span class="w">
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[[</span><span class="n">?e</span><span class="w"> </span><span class="no">:age</span><span class="w"> </span><span class="mi">42</span><span class="p">]</span><span class="w">
         </span><span class="p">(</span><span class="nb">not</span><span class="w"> </span><span class="p">[</span><span class="n">?e</span><span class="w"> </span><span class="no">:likes</span><span class="w"> </span><span class="s">"icecream"</span><span class="p">]]}</span><span class="w">
</span></code></pre></div></div>
<p>This will find us people of age 42 who don’t like ice cream. Be aware that a <code class="language-plaintext highlighter-rouge">not</code> works like an anti-join than an actual negation of facts. For example you cannot write the query</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">?e</span><span class="p">]</span><span class="w">
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[(</span><span class="nb">not</span><span class="w"> </span><span class="p">[</span><span class="n">?e</span><span class="w"> </span><span class="no">:likes</span><span class="w"> </span><span class="s">"icecream"</span><span class="p">]]}</span><span class="w">
</span></code></pre></div></div>
<p>to find people who don’t like ice cream. This is a bit contrary to classical literature Datalog where this query would be accepted.</p>
<h3 id="predicates-and-functions">Predicates and Functions</h3>
<p>Predicates are used to filter matching tuples and functions are used to create new join variables.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">?e</span><span class="w"> </span><span class="n">?birth-year</span><span class="p">]</span><span class="w">
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[[</span><span class="n">?e</span><span class="w"> </span><span class="no">:person/age</span><span class="w"> </span><span class="n">?age</span><span class="p">]</span><span class="w">
         </span><span class="p">[(</span><span class="nb">&gt;</span><span class="w"> </span><span class="n">?age</span><span class="w"> </span><span class="mi">30</span><span class="p">)]</span><span class="w">
         </span><span class="p">[(</span><span class="nb">-</span><span class="w"> </span><span class="mi">2026</span><span class="w"> </span><span class="n">?age</span><span class="p">)</span><span class="w"> </span><span class="n">?birth-year</span><span class="p">]]}</span><span class="w">
</span></code></pre></div></div>
<p>This finds us people older than 30 and their birth year. The second where clause is a predicate filter and the final clause creates the birth year variable.</p>

<p>This gives you a little introduction tour of EDN Datalog. I have not touched rules which are the most powerful aspect of Datalog.</p>

<p>There are lots of Triplox parts that I explicitly left out for now. This includes the history aspects of the data model and Rich’s <a href="https://www.youtube.com/watch?v=D6nYfttnVco">database as a value</a> concept, APIs like <a href="https://docs.datomic.com/query/query-pull.html">pull</a>, which allows you to do graph traversals and the <a href="https://docs.datomic.com/reference/entities.html">entity API</a>. I want to focus on transactions and queries in a first Triplox version. There is also lots to be written about for incremental Datalog queries. So stay tuned for more updates about Triplox.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Example shamelessly stolen from Jepsen’s Datomic report. <a href="https://jepsen.io/analyses/datomic-pro-1.0.7075">https://jepsen.io/analyses/datomic-pro-1.0.7075</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>Stolen from the Datomic docs: <a href="https://docs.datomic.com/query/query-executing.html#unification">https://docs.datomic.com/query/query-executing.html#unification</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Finn Völkel</name></author><summary type="html"><![CDATA[I am working on a Datalog database engine à la Datomic on top of object storage. The system is called Triplox (a portemanteau of Triple and Blocks). In an attempt to become a better communicator I decided to start a little log to explain some concepts in Triplox.]]></summary></entry><entry><title type="html">More thoughts on N-way joins in DBSP</title><link href="https://finnvolkel.com/more-thoughts-on-n-way-joins-in-dbsp" rel="alternate" type="text/html" title="More thoughts on N-way joins in DBSP" /><published>2026-04-10T00:00:00+00:00</published><updated>2026-04-10T00:00:00+00:00</updated><id>https://finnvolkel.com/more-thoughts-on-n-way-joins-in-dbsp</id><content type="html" xml:base="https://finnvolkel.com/more-thoughts-on-n-way-joins-in-dbsp"><![CDATA[<p><em>Thoughts on N-way joins in DBSP and WCOJ in DBSP after some discussion on the <a href="https://github.com/feldera/feldera">Feldera</a> Discord server</em></p>

<p>I used the following recursive formula for multi-joins in <a href="wcoj-dbsp-zsets-and-datalog#efficient-multi-joins-in-dbsp">Efficient multi-joins in DBSP</a></p>

\[\begin{align*}
\Delta_{1..i} &amp;= \Delta_{1..i-1} \bowtie \Delta R_i^A \\
              &amp;+ \Delta_{1..i-1} \bowtie (R_i^A)^{-1} \\
              &amp;+ \Delta R_i^A \bowtie (R_1^A)^{-1} \bowtie \dots \bowtie (R_{i-1}^A)^{-1}
\end{align*}\]

<p>From a discussion on the Feldera Discord<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> I learned that Feldera uses the following formula for multi-way joins. For completeness $A^\Delta$ is the delta for relation $A$ arriving at the current batch and $A^{-1}$ is $A$ just before the current batch has been processed. Let’s say you want to compute the incremental query $(A \bowtie B \bowtie C)^\Delta$. This can be written as</p>

\[A^\Delta \bowtie B \bowtie C \enspace + \enspace A^{-1} \bowtie B^\Delta \bowtie C \enspace + \enspace A^{-1} \bowtie B^{-1} \bowtie C^\Delta\]

<p>you obtain the above by “collapsing” certain terms together from the fully expanded version using the bilinear formula.  The first term is obtained by collapsing</p>

\[\begin{align*}
A^\Delta \bowtie B^\Delta \bowtie C^\Delta \enspace &amp;+ \enspace A^\Delta \bowtie B^{-1} \bowtie C^\Delta \enspace + \enspace \\ 
A^\Delta \bowtie B^\Delta \bowtie C^{-1} \enspace &amp;+ \enspace
A^\Delta \bowtie B^{-1} \bowtie C^{-1}
\end{align*}\]

<p>Any terms involving $A^\Delta$ are merged into the first term. For the second you merge everything that contains a $B^\Delta$ but no $A^\Delta$ and so forth. The formula for an N-way join then becomes</p>

\[\sum_{i \in [n]} R_1^{-1}\dots R_{i-1}^{-1} R_i^\Delta R_{i+1} \dots R_n\]

<p>I am abusing the Cartesian product notation here for join. I mainly wondered if we could make use of this kind of “collapsing” as well in the recursive formula above (by collapsing the first two terms)</p>

\[\begin{align*}
\Delta_{1..i} &amp;= \Delta_{1..i-1} \bowtie R_i^A \\
              &amp;+ \Delta R_i^A \bowtie (R_1^A)^{-1} \bowtie \dots \bowtie (R_{i-1}^A)^{-1}
\end{align*}\]

<p>The problem is that we still need $(R_{i-1}^A)^{-1}$ terms in later iterations, so we can’t simply merge $R_i^{\Delta}$ into $R_i^{-1}$ as we still need the $R_i^{-1}$ later on.</p>

<p>If you flip the order around (this is just a renaming of relations), you obtain:</p>

\[\sum_{i \in [n]} R_i^\Delta R_1 \dots R_{i-1}  R_{i+1}^{-1} \dots R_n^{-1}\]

<p>I wonder if this is actually simpler to implement than the recursive formula above. After having processed $R_i^{\Delta}$, you merge $R_i^{\Delta}$ into $R_i^{-1}$ to obtain $R_i$. This works because in subsequent iterations of that loop $R_i^{-1}$ is no longer needed. The main difference for us vs. the Feldera folks is that we are not hardcoding (compiling) the circuit at this level based on the number of relations involved. We want this circuit component to work for an arbitrary number of relations (essentially the triple patterns of our Datalog queries). Feldera would have for example 3 different circuit parts for the $(A \bowtie B \bowtie C)^\Delta$ query above , whereas our algorithm is the same no matter if there are 3, 4 or 5 relations involved.</p>

<p>In the limit the recursive formula does fewer joins (only about half as many). The problem is that this only amortizes after 5 relations are involved, before that the simpler formula does <em>fewer</em> joins. So this is something to verify and hopefully also run some benchmarks against. I suspect that the simpler formula might actually be faster in practice.</p>

<p>For Feldera there is actually an optimisation problem to be solved here. In our EDN Datalog case we have all possible permutations of our <a href="https://docs.datomic.com/indexes/indexes.html">covering indices</a> at hand and so prefix lookups on the permuations of the covering index work not matter the variable order. Feldera doesn’t have this luxury. So they might need to build extra indices for joining certain terms. If you now add parallelisation to the mix (joining data on different workers), you need to also decide which (potentially newly build) index permutations do you send to which worker. This is I think a good topic for another blog post. That’s all for now.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://discord.com/channels/1223851723110748251/1223851723643293729/1458497257304363186">https://discord.com/channels/1223851723110748251/1223851723643293729/1458497257304363186</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Finn Völkel</name></author><summary type="html"><![CDATA[Thoughts on N-way joins in DBSP and WCOJ in DBSP after some discussion on the Feldera Discord server]]></summary></entry><entry><title type="html">WCOJ - WCOJ meets DBSP</title><link href="https://finnvolkel.com/wcoj-wcoj-meets-dbsp" rel="alternate" type="text/html" title="WCOJ - WCOJ meets DBSP" /><published>2026-01-03T00:00:00+00:00</published><updated>2026-01-03T00:00:00+00:00</updated><id>https://finnvolkel.com/wcoj-wcoj-meets-dbsp</id><content type="html" xml:base="https://finnvolkel.com/wcoj-wcoj-meets-dbsp"><![CDATA[<p>Edit(2026-04-30): After having thought some more about this post there a multiple issues. The algorithm is not correct and the claim about WCOJ needs to be more precise. There are multiple definitions for worst-case optimality in the context of IVM and I need to get them straight before actually claiming anything. The more serious issue is correctness. Let $P_{i}$ be the set of prefixes at level $i$ of the algorithm below and $E_{i}$ be the set of extensions, then</p>

\[\Delta P_{i+1}
=
\operatorname{Extend}_i(\Delta P_i, E_i^{\mathrm{old}})
+
\operatorname{Extend}_i(P_i^{\mathrm{old}}, \Delta E_i)
+
\operatorname{Extend}_i(\Delta P_i, \Delta E_i)\]

<p>or in compact form</p>

\[\Delta P_{i+1}
=
\operatorname{Extend}_i(\Delta P_i, E_i^{\mathrm{new}})
+
\operatorname{Extend}_i(P_i^{\mathrm{old}}, \Delta E_i)\]

<p>In my main loop below I am only considering $\Delta P_{i}$. I am working on an updated version.</p>

<p><em>Putting together almost everything from the series to create an efficient (complexity-wise) WCOJ algorithm for DBSP.</em></p>

<p>Some parts of this post have not been heavily scrutinised (by other people), so the content needs to be taken with a grain of salt. It wouldn’t surprise me if something is not quite correct or could be simplified further. I will also abuse some notation a bit. I couldn’t find any reference of combining <a href="https://arxiv.org/abs/2203.16684">DBSP</a> with <a href="https://en.wikipedia.org/wiki/Worst-case_optimal_join_algorithm">WCOJ</a> in the literature. The only reference that seems relevant is a paper of how to do incremental view maintenance (IVM - the general problem DBSP addresses) for the WCOJ algorithm Leapfrog-Triejoin <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> and how to do WCOJs in the more powerful incremental computation framework differential dataflow<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>.</p>

<p>Before we start, let’s take a step back and think about what WCOJ for DBSP means. WCOJ in the non-incremental case assures us that we are not doing more work than the largest possible output size of a query ($O(N^{3/2})$ for the triangle query).  Combining WCOJ with DBSP means that we are not doing more work than the largest possible output change for a fixed incremental query $Q$ and some changes $\Delta R_i$. Coming back to our beloved triangle query, adding or removing edges in a transaction, can in the <em>worst case</em> lead to $|\Delta triangle|$ being removed or added. In a WCOJ DBSP algorithm we would do never more work than $O(|\Delta triangle|)$ per DBSP tick.</p>

<p>At a high-level we use the multi-join formula from the <a href="wcoj-dbsp-zsets-and-datalog">previous post</a>  at each level of the <a href="wcoj-generic-join">Generic Join</a> algorithm. Consider the triangle query</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">?a</span><span class="w"> </span><span class="n">?b</span><span class="w"> </span><span class="n">?c</span><span class="p">]</span><span class="w">
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[[</span><span class="n">?a</span><span class="w"> </span><span class="no">:g/to</span><span class="w"> </span><span class="n">?b</span><span class="p">]</span><span class="w">   </span><span class="c1">;; relation R</span><span class="w">
         </span><span class="p">[</span><span class="n">?a</span><span class="w"> </span><span class="no">:g/to</span><span class="w"> </span><span class="n">?c</span><span class="p">]</span><span class="w">   </span><span class="c1">;; relation S</span><span class="w">
         </span><span class="p">[</span><span class="n">?b</span><span class="w"> </span><span class="no">:g/to</span><span class="w"> </span><span class="n">?c</span><span class="p">]]}</span><span class="w"> </span><span class="c1">;; relation T</span><span class="w">
</span></code></pre></div></div>
<p>Let’s say we have the join variable order <code class="language-plaintext highlighter-rouge">(?a ?b ?c)</code>. As there are only two relations involved per variable the multi-join formula reduces to the standard join formula</p>

\[% Full theorem statement:
(R \bowtie S)^\Delta = R^\Delta \bowtie S^\Delta + R^{-1} \bowtie S^\Delta + R^\Delta \bowtie S^{-1}\]

<p>where $A^{-1}$ means the relation at the previous tick (the relation delayed by one timestamp).</p>

<p>First we produce a Z-Set containing the prefixes $(a)$ for the results $a$ values appearing in both $R$ and $S$ . The problem is that $R$, $R^\Delta$, $S$ and $S^\Delta$ are Z-sets defined over complete tuples, not key prefixes of $(a)$. The relation $R$ is indexed over tuples $(a, b)$. At the variable-$a$ level, we can enumerate which $a$ values exist, but there’s no single weight for $a$ , but only for complete $(a, b)$ pairs.</p>

<p>We resolve this by treating intermediate levels as unweighted key sets.
When doing the first level (variable $a$) in the join process, we’ll actually 
use a weight of 1 (the multiplicative identity) and these weights are effectively transparent in the join process.  Only when binding the <em>final variable</em> of a relation do we use the actual Z-set weights.</p>

<p>For the triangle query with variable order <code class="language-plaintext highlighter-rouge">(?a ?b ?c)</code>:</p>

<ul>
  <li>$R$’s weights apply when binding <code class="language-plaintext highlighter-rouge">?b</code> (its final variable in the AEV ordering)</li>
  <li>$S$’s and $T$’s weights apply when binding <code class="language-plaintext highlighter-rouge">?c</code></li>
</ul>

<p>This ensures each relation’s weight contributes exactly once to the final result weight. To see how this weight-separation works in practice, let’s first look at how we represent partial results during the join process.</p>
<h3 id="result-representation-as-indexed-z-sets">Result representation as Indexed Z-Set’s</h3>
<p>In the relational model result sets are often represented as lists of lists. For example consider the following result set of geo-data that becomes finer grained from left to right.
<img src="/assets/flat_listing.png" alt="flat_listing" />
In a trie representation (also used in factorized databases) the above would look as follows. 
<img src="/assets/trie_view5.png" alt="trie_view5" />
If <code class="language-plaintext highlighter-rouge">Country</code>, <code class="language-plaintext highlighter-rouge">City</code> and <code class="language-plaintext highlighter-rouge">District</code> were all join variables in a WCOJ join, this trie structure would be build up as we are joining. Leapfrog-Triejoin<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup> builds this structure in a DFS manner and GenericJoin<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup> walks this structure in BFS manner (probably good content for another post). When one extends one prefix with extensions in <a href="wcoj-generic-join">GenericJoin</a>, it essentially means extending a subtree by one level. For example for the prefix <code class="language-plaintext highlighter-rouge">(Germany, Berlin)</code> one would create new prefixes <code class="language-plaintext highlighter-rouge">(Germany, Berlin, Mitte)</code> and <code class="language-plaintext highlighter-rouge">(Germany, Berlin, Kreuzberg)</code>. When using persistent lists to represent result tuples in GenericJoin the trie is implicitly encoded by the structural sharing of the persistent lists<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>.</p>

<p>We are going to use indexed Z-sets to represent our results when doing the variable based WCOJ join. Each level of the result trie are the results for one join variable. Paths to leaves represent prefixes. The weight at each leaf is the product of weights from all relations whose variables have been fully bound. Meaning only when the last variable of a relation is being joined the actual weight is being used in the join process (otherwise we use the multiplicative identity 1).</p>

<p>To implement our incremental WCOJ, we need three key operations on indexed Z-sets that let us build up result tries level-by-level while managing weights correctly.</p>
<h4 id="extendleaves"><code class="language-plaintext highlighter-rouge">extendLeaves</code></h4>
<p>We add the following helper methods on our <a href="https://github.com/FiV0/hooray2/blob/ff636efe701ce9edf428b6df79e6fda9ee15340a/src/main/kotlin/org/hooray/incremental/IZSet.kt"><code class="language-plaintext highlighter-rouge">IZSet</code></a> (a generic interface implemented by both Z-sets and indexed Z-sets) implementation.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typealias</span> <span class="nc">Prefix</span> <span class="p">=</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Any</span><span class="p">&gt;</span>

<span class="k">fun</span> <span class="nf">extendLeaves</span><span class="p">(</span><span class="n">mapFn</span><span class="p">:</span> <span class="p">(</span><span class="nc">Prefix</span><span class="p">,</span> <span class="nc">W</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">ZSet</span><span class="p">&lt;</span><span class="nc">K</span><span class="p">,</span> <span class="nc">W</span><span class="p">&gt;):</span> <span class="nc">IndexedZSet</span><span class="p">&lt;</span><span class="nc">K</span><span class="p">,</span> <span class="nc">W</span><span class="p">&gt;</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">extendLeaves</code> works on a Z-set (can be a standard z-set or a (nested) indexed-zset). Given a function that maps from the prefix path of the partial result and the weight to a Z-set, it creates a new indexed Z-set where the prefix has been extended by one level. Using the notation from the <a href="wcoj-dbsp-zsets-and-datalog">previous post</a> and a partial result tree from above with added weights</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="s">"Germany"</span><span class="n">,</span><span class="w"> 
  </span><span class="p">{</span><span class="s">"Berlin"</span><span class="w"> </span><span class="mi">1</span><span class="w"> 
   </span><span class="s">"Munich"</span><span class="w"> </span><span class="mi">-1</span><span class="p">}</span><span class="w"> 
 </span><span class="s">"USA"</span><span class="w"> 
  </span><span class="p">{</span><span class="s">"NYC"</span><span class="w"> </span><span class="mi">-1</span><span class="w"> 
   </span><span class="s">"LA"</span><span class="w"> </span><span class="mi">1</span><span class="p">}}</span><span class="w">
</span></code></pre></div></div>
<p>On this example our <code class="language-plaintext highlighter-rouge">extendLeaves</code> function would receive</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="s">"Germany"</span><span class="n">,</span><span class="w"> </span><span class="s">"Berlin"</span><span class="p">]</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="p">[</span><span class="s">"Germany"</span><span class="n">,</span><span class="w"> </span><span class="s">"Munich"</span><span class="p">]</span><span class="w"> </span><span class="mi">-1</span><span class="w">
</span><span class="p">[</span><span class="s">"USA"</span><span class="n">,</span><span class="w"> </span><span class="s">"NYC"</span><span class="p">]</span><span class="w"> </span><span class="mi">-1</span><span class="w">
</span><span class="p">[</span><span class="s">"USA"</span><span class="n">,</span><span class="w"> </span><span class="s">"LA"</span><span class="p">]</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></code></pre></div></div>
<p>If the mapping function would take the first character of the city key and multiply the weight by two to create a new singleton Z-Set, we would obtain</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="s">"Germany"</span><span class="n">,</span><span class="w"> 
  </span><span class="p">{</span><span class="s">"Berlin"</span><span class="w"> </span><span class="p">{</span><span class="s">"B"</span><span class="w"> </span><span class="mi">2</span><span class="p">}</span><span class="w"> 
   </span><span class="s">"Munich"</span><span class="w"> </span><span class="p">{</span><span class="s">"M"</span><span class="w"> </span><span class="mi">-2</span><span class="p">}}</span><span class="w"> 
 </span><span class="s">"USA"</span><span class="w"> 
  </span><span class="p">{</span><span class="s">"NYC"</span><span class="w"> </span><span class="p">{</span><span class="s">"N"</span><span class="w"> </span><span class="mi">-2</span><span class="p">}</span><span class="w"> 
   </span><span class="s">"LA"</span><span class="w"> </span><span class="p">{</span><span class="s">"L"</span><span class="w"> </span><span class="mi">2</span><span class="p">}}}</span><span class="w">
</span></code></pre></div></div>
<h4 id="getbyprefix-and-aszsetview"><code class="language-plaintext highlighter-rouge">getByPrefix</code> and <code class="language-plaintext highlighter-rouge">asZSetView</code></h4>
<p>The <code class="language-plaintext highlighter-rouge">getByPrefix</code> function returns Z-set at the given prefix path.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">getByPrefix</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">):</span> <span class="nc">IZSet</span><span class="p">&lt;</span><span class="nc">K</span><span class="p">,</span> <span class="nc">W</span><span class="p">,</span> <span class="err">*</span><span class="p">&gt;</span>
</code></pre></div></div>
<p>For the full hierarchical geo-data example from above:</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="s">"Germany"</span><span class="n">,</span><span class="w">
  </span><span class="p">{</span><span class="s">"Berlin"</span><span class="w"> 
    </span><span class="p">{</span><span class="s">"Mitte"</span><span class="w"> </span><span class="mi">-1</span><span class="w">
     </span><span class="s">"Kreuzberg"</span><span class="w"> </span><span class="mi">1</span><span class="p">}</span><span class="w"> 
   </span><span class="s">"Munich"</span><span class="w"> 
    </span><span class="p">{</span><span class="s">"Schwabing"</span><span class="w"> </span><span class="mi">1</span><span class="p">}}</span><span class="w"> 
 </span><span class="s">"USA"</span><span class="w"> </span><span class="n">...</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>for the prefix <code class="language-plaintext highlighter-rouge">("Germany", "Berlin")</code>, one would obtain</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="s">"Mitte"</span><span class="w"> </span><span class="mi">-1</span><span class="w">
 </span><span class="s">"Kreuzberg"</span><span class="w"> </span><span class="mi">1</span><span class="p">}</span><span class="w"> 
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">asZSetView</code> function returns the unweighted key set described above when called on an indexed Z-set and just the Z-set itself when called on a standard Z-set.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">asZSetView</span><span class="p">():</span> <span class="nc">ZSet</span><span class="p">&lt;</span><span class="nc">K</span><span class="p">,</span> <span class="nc">W</span><span class="p">&gt;</span>
</code></pre></div></div>
<p>For the full geo-data example Z-set we would get</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="s">"Germany"</span><span class="w"> </span><span class="mi">1</span><span class="w">
 </span><span class="s">"USA"</span><span class="w"> </span><span class="mi">1</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>at the top-level and if we chain <code class="language-plaintext highlighter-rouge">getByPrefix</code> and <code class="language-plaintext highlighter-rouge">zSetView</code></p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">zset</span><span class="p">.</span><span class="nf">getByPrefix</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="s">"Germany"</span><span class="p">)).</span><span class="nf">asZSetView</span><span class="p">()</span>
</code></pre></div></div>
<p>we’d get</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="s">"Berlin"</span><span class="w"> </span><span class="mi">1</span><span class="w">
 </span><span class="s">"Munich"</span><span class="w"> </span><span class="mi">1</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>In our DBSP WCOJ algorithm, indexed Z-sets are fundamentally a structural/organizational layer, they are kind of a trie that partitions the data by a key. At intermediate layers we don’t carry weight information, so the chaining of <code class="language-plaintext highlighter-rouge">getByPrefix</code> and <code class="language-plaintext highlighter-rouge">asZSetView</code> allows us to obtain Z-Sets with natural weights at intermediate levels and leaf-level weights when the final variable of a relation gets joined.</p>
<h3 id="incrementalprefixextender">IncrementalPrefixExtender</h3>
<p>The corresponding incremental <code class="language-plaintext highlighter-rouge">PrefixExtender</code> interface from <a href="wcoj-generic-join">WCOJ - Generic Join</a> is given by</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">ZSetPrefixExtender</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">count</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">):</span> <span class="nc">Int</span>
    <span class="k">fun</span> <span class="nf">propose</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">):</span> <span class="nc">ZSet</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">,</span> <span class="nc">IntegerWeight</span><span class="p">&gt;</span>
    <span class="k">fun</span> <span class="nf">intersect</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">,</span> <span class="n">extensions</span><span class="p">:</span> <span class="nc">ZSet</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">,</span> <span class="nc">IntegerWeight</span><span class="p">&gt;):</span> <span class="nc">ZSet</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">,</span> <span class="nc">IntegerWeight</span><span class="p">&gt;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>In intermediate Z-Set operations of a DBSP pipeline it might happen that the multiplicities become quite large. To guard against the possibility of an overflow we use <code class="language-plaintext highlighter-rouge">IntegerWeight</code> , which is a custom implementation of <code class="language-plaintext highlighter-rouge">int</code>. 
A <code class="language-plaintext highlighter-rouge">ZSetPrefixExtender</code> at the base level (when we use a base data pattern à la <code class="language-plaintext highlighter-rouge">[?a :g/to ?b]</code>) in the algorithm will be backed by an indexed Z-set of one of the permutations of the EAV index. At each timestamp tick both  $EAV^\Delta$ and $EAV^{-1}$ (or some other permutation of a base index).
From the fixed part of the query and a given prefix we look up the nested (indexed) Z-set. The size of this Z-set is the <code class="language-plaintext highlighter-rouge">count</code> of potential extensions. <code class="language-plaintext highlighter-rouge">propose</code>  returns the Z-Set view of the index via chained <code class="language-plaintext highlighter-rouge">getByPrefix</code> and <code class="language-plaintext highlighter-rouge">zSetView</code> calls. <code class="language-plaintext highlighter-rouge">intersect</code> does an equi-join with this Z-set view and the passed in Z-set.</p>
<h3 id="incrementalindex">IncrementalIndex</h3>
<p>In the DBSP framework the operators work on streams (a sequence of Z-sets) and this is definitely the better abstraction if you want to implement rules and recursive queries. As I mainly wanted to get something correct working for standard EDN Datalog data patterns, I opted for a simpler <code class="language-plaintext highlighter-rouge">eval/commit</code> approach. <code class="language-plaintext highlighter-rouge">eval</code> receives the Z-sets from upstream operators or the changed base level relations and <code class="language-plaintext highlighter-rouge">commit</code> updates the state of the operators with the recent changes. This works well for tree-like pipelines (no recursive queries) as the time dimension is strictly ordered. One <code class="language-plaintext highlighter-rouge">eval</code> + <code class="language-plaintext highlighter-rouge">commit</code> call per tick.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">IncrementalIndex</span> <span class="p">:</span> <span class="nc">LevelParticipation</span> <span class="p">{</span>
    <span class="cm">/** Receive the delta for this transaction */</span>
    <span class="k">fun</span> <span class="nf">receiveDelta</span><span class="p">(</span><span class="n">delta</span><span class="p">:</span> <span class="nc">ZSetIndices</span><span class="p">)</span>

    <span class="cm">/** Merge current delta into accumulated state (call after join completes) */</span>
    <span class="k">fun</span> <span class="nf">commit</span><span class="p">()</span>

    <span class="cm">/** Extender over the current delta */</span>
    <span class="kd">val</span> <span class="py">delta</span><span class="p">:</span> <span class="nc">ZSetPrefixExtender</span>

    <span class="cm">/** Extender over z⁻¹ (accumulated previous state) */</span>
    <span class="kd">val</span> <span class="py">accumulated</span><span class="p">:</span> <span class="nc">ZSetPrefixExtender</span>
<span class="p">}</span>
</code></pre></div></div>

<p>An <code class="language-plaintext highlighter-rouge">IncrementalIndex</code> receives the updates (<code class="language-plaintext highlighter-rouge">ZSetIndices</code>) to the base indices via <code class="language-plaintext highlighter-rouge">receiveDelta</code>. For composite <a href="wcoj-datalog-and-genericjoin">Datalog Connectors</a> the <code class="language-plaintext highlighter-rouge">IncrementalIndex</code> passes the updates to it’s children. It further exposes the <code class="language-plaintext highlighter-rouge">delta</code> and the <code class="language-plaintext highlighter-rouge">accumulated</code> state of the current iteration as <code class="language-plaintext highlighter-rouge">ZSetPrefixExtender</code>. After the changes have been processed the accumulated state gets updated via the <code class="language-plaintext highlighter-rouge">commit</code> method.</p>

<p>With all of this machinery in place we can finally turn to a WCOJ DBSP algorithm.</p>
<h3 id="incrementalgenericjoin">IncrementalGenericJoin</h3>
<p>The code for the <code class="language-plaintext highlighter-rouge">IncrementalGenericJoin</code> is now pretty straightforward (at least to me 😅).</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">IncrementalGenericJoin</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">relations</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">IncrementalIndex</span><span class="p">&gt;,</span> <span class="k">private</span> <span class="kd">val</span> <span class="py">levels</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">:</span> <span class="nc">IncrementalJoin</span><span class="p">&lt;</span><span class="nc">ResultTuple</span><span class="p">&gt;</span> <span class="p">{</span>
    <span class="cm">/**
     * Compute delta extensions for one level using the DBSP incremental formula:
     *
     * Δ_{1..n} builds up as: Δ_{1..i} = Δ_{1..i-1} ⋈ Δᵢ
     *                                 + Δ_{1..i-1} ⋈ z⁻¹(i)
     *                                 + Δᵢ ⋈ z⁻¹(processed relations)
     */</span>
    <span class="k">private</span> <span class="k">fun</span> <span class="nf">computeLevelDelta</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">,</span> <span class="n">relations</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">IncrementalIndex</span><span class="p">&gt;):</span> <span class="nc">ZSet</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">,</span> <span class="nc">IntegerWeight</span><span class="p">&gt;</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">relations</span><span class="p">.</span><span class="nf">isEmpty</span><span class="p">())</span> <span class="k">return</span> <span class="nc">ZSet</span><span class="p">.</span><span class="nf">empty</span><span class="p">()</span>

        <span class="c1">// Start with smallest delta (WCOJ optimization)</span>
        <span class="kd">val</span> <span class="py">minIndex</span> <span class="p">=</span> <span class="n">relations</span><span class="p">.</span><span class="n">indices</span><span class="p">.</span><span class="nf">minBy</span> <span class="p">{</span> <span class="n">relations</span><span class="p">[</span><span class="n">it</span><span class="p">].</span><span class="n">delta</span><span class="p">.</span><span class="nf">count</span><span class="p">(</span><span class="n">prefix</span><span class="p">)</span> <span class="p">}</span>
        <span class="kd">var</span> <span class="py">runningDelta</span> <span class="p">=</span> <span class="n">relations</span><span class="p">[</span><span class="n">minIndex</span><span class="p">].</span><span class="n">delta</span><span class="p">.</span><span class="nf">propose</span><span class="p">(</span><span class="n">prefix</span><span class="p">)</span>

        <span class="k">for</span> <span class="p">(</span><span class="n">j</span> <span class="k">in</span> <span class="n">relations</span><span class="p">.</span><span class="n">indices</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">j</span> <span class="p">==</span> <span class="n">minIndex</span><span class="p">)</span> <span class="k">continue</span>

            <span class="kd">val</span> <span class="py">deltaJ</span> <span class="p">=</span> <span class="n">relations</span><span class="p">[</span><span class="n">j</span><span class="p">].</span><span class="n">delta</span><span class="p">.</span><span class="nf">propose</span><span class="p">(</span><span class="n">prefix</span><span class="p">)</span>
            <span class="kd">var</span> <span class="py">term3</span> <span class="p">=</span> <span class="n">deltaJ</span>

            <span class="c1">// Intersect with z⁻¹ of all relations processed before j</span>
            <span class="k">for</span> <span class="p">(</span><span class="n">k</span> <span class="k">in</span> <span class="mi">0</span> <span class="n">until</span> <span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">term3</span> <span class="p">=</span> <span class="n">relations</span><span class="p">[</span><span class="n">k</span><span class="p">].</span><span class="n">accumulated</span><span class="p">.</span><span class="nf">intersect</span><span class="p">(</span><span class="n">prefix</span><span class="p">,</span> <span class="n">term3</span><span class="p">)</span>
            <span class="p">}</span>

            <span class="c1">// Don't forget minIndex if it comes after j</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">minIndex</span> <span class="p">&gt;</span> <span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">term3</span> <span class="p">=</span> <span class="n">relations</span><span class="p">[</span><span class="n">minIndex</span><span class="p">].</span><span class="n">accumulated</span><span class="p">.</span><span class="nf">intersect</span><span class="p">(</span><span class="n">prefix</span><span class="p">,</span> <span class="n">term3</span><span class="p">)</span>
            <span class="p">}</span>

                           <span class="c1">// Δ_{1..i-1} ⋈ Δᵢ</span>
            <span class="n">runningDelta</span> <span class="p">=</span> <span class="n">runningDelta</span><span class="p">.</span><span class="nf">equiJoin</span><span class="p">(</span><span class="n">deltaJ</span><span class="p">)</span> <span class="p">+</span>
                           <span class="c1">// + Δ_{1..i-1} ⋈ z⁻¹(i)</span>
                           <span class="n">relations</span><span class="p">[</span><span class="n">j</span><span class="p">].</span><span class="n">accumulated</span><span class="p">.</span><span class="nf">intersect</span><span class="p">(</span><span class="n">prefix</span><span class="p">,</span> <span class="n">runningDelta</span><span class="p">)</span> <span class="p">+</span>
                           <span class="c1">// + Δᵢ ⋈ z⁻¹(1) ⋈ z⁻¹(2) ⋈ ... ⋈ z⁻¹(i-1)</span>
                           <span class="n">term3</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="n">runningDelta</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">join</span><span class="p">(</span><span class="n">deltas</span><span class="p">:</span> <span class="nc">ZSetIndices</span><span class="p">):</span> <span class="nc">ZSet</span><span class="p">&lt;</span><span class="nc">ResultTuple</span><span class="p">,</span> <span class="nc">IntegerWeight</span><span class="p">&gt;</span> <span class="p">{</span>
        <span class="c1">// 1. Distribute deltas</span>
        <span class="n">relations</span><span class="p">.</span><span class="nf">forEach</span> <span class="p">{</span> <span class="n">rel</span> <span class="p">-&gt;</span> <span class="n">rel</span><span class="p">.</span><span class="nf">receiveDelta</span><span class="p">(</span><span class="n">deltas</span><span class="p">)</span> <span class="p">}</span>

        <span class="c1">// 2. Compute join level by level</span>
        <span class="kd">val</span> <span class="py">participatingLevel1</span> <span class="p">=</span> <span class="n">relations</span><span class="p">.</span><span class="nf">filter</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">participatesInLevel</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="p">}</span>
        <span class="kd">var</span> <span class="py">result</span><span class="p">:</span> <span class="nc">IZSet</span><span class="p">&lt;</span><span class="nc">Any</span><span class="p">,</span> <span class="nc">IntegerWeight</span><span class="p">,</span> <span class="err">*</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">computeLevelDelta</span><span class="p">(</span><span class="nf">emptyList</span><span class="p">(),</span> <span class="n">participatingLevel1</span><span class="p">)</span>

        <span class="k">for</span> <span class="p">(</span><span class="n">level</span> <span class="k">in</span> <span class="mi">1</span> <span class="n">until</span> <span class="n">levels</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">participating</span> <span class="p">=</span> <span class="n">relations</span><span class="p">.</span><span class="nf">filter</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">participatesInLevel</span><span class="p">(</span><span class="n">level</span><span class="p">)</span> <span class="p">}</span>
            <span class="n">result</span> <span class="p">=</span> <span class="n">result</span><span class="p">.</span><span class="nf">extendLeaves</span> <span class="p">{</span> <span class="n">prefix</span><span class="p">,</span> <span class="n">weight</span> <span class="p">-&gt;</span>
                <span class="nf">computeLevelDelta</span><span class="p">(</span><span class="n">prefix</span><span class="p">,</span> <span class="n">participating</span><span class="p">).</span><span class="nf">multiply</span><span class="p">(</span><span class="n">weight</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>

        <span class="c1">// 3. Commit deltas</span>
        <span class="n">relations</span><span class="p">.</span><span class="nf">forEach</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span> <span class="p">}</span>

        <span class="k">return</span> <span class="n">result</span>
    <span class="p">}</span>
<span class="p">}</span>

</code></pre></div></div>
<p>The join receives the deltas and first distributes them across the <code class="language-plaintext highlighter-rouge">IncrementalIndex</code> relations. Afterwards the algorithm uses the multi-join formula from the <a href="wcoj-dbsp-zsets-and-datalog#efficient-multi-joins-in-dbsp">previous post</a> to extend (via <code class="language-plaintext highlighter-rouge">extendLeaves</code>) a partial result trie level by level via <code class="language-plaintext highlighter-rouge">computeLevelDelta</code> . <code class="language-plaintext highlighter-rouge">computeLevelDelta</code> is just the multi-join formula enshrined in code, but it also receives the prefix of the current sub-tree being extended so that the <code class="language-plaintext highlighter-rouge">ZSetPrefixExtender</code>s can constrain their respective Z-sets. After the Z-Set of the new level is computed it gets scalar multiplied by the weight of the sub-tree currently being extended. Until the result set (result trie) has been fully built up, the weight of the partial result trie leafs is the contribution of all relations where all variables have been fully exhausted in the WCOJ process. If one takes the triangle query again</p>

\[Q(A,B,C) = R(A,B) \bowtie S(B,C) \bowtie T(C,A)\]

<p>When only variables $A$ and $B$ have been joined so far, the result trie leaf weights will only contain the weight contribution from $R$ even if the $(a, b)$ prefixes have also been validated against $S$ and $T$ respectively. Their weight contribution applies when $C$ gets joined.</p>

<p>Please note that all of the implementations above are purely educational, meaning that all of this will likely have bad constants in practice. This is WCO in terms of big $O$ complexity, but might fare badly in practice and you may be better off doing good old binary joins.</p>

<p>I think it would be good to fully formalize this WCOJ algorithm (or some simplification thereof) as a DBSP circuit.</p>

<p>The central premise of this post is that doing WCOJ in DBSP is worth it. Datomic style engines are mostly entity-centric and for most use cases don’t have cyclic queries (where WCOJ shines). If you’d want graph data pattern matching you likely reach for a graph database (the algorithm described here still applies, but the indices might be quite different).<br />
The incoming deltas are likely quite small, so is the cost  (likely high constants) incurred by all the complex maintenance and joins of Z-sets worth it, or would a left-deep join tree with good statistics fare almost always better? Something to figure out.</p>

<p>This was the post I wanted to get at when starting the <a href="wcoj-graph-join-correspondence">WCOJ</a> series. There is more to write about, so I guess there will be more posts, but it might be a while until I get to them.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://arxiv.org/abs/1303.5313">https://arxiv.org/abs/1303.5313</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://github.com/frankmcsherry/dataflow-join">https://github.com/frankmcsherry/dataflow-join</a> - this is not actually implemented in Differential Dataflow (DD), but rather Timely Dataflow which is the backbone of DD. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p><a href="https://arxiv.org/abs/1210.0481">https://arxiv.org/abs/1210.0481</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p><a href="https://arxiv.org/abs/1310.3314">https://arxiv.org/abs/1310.3314</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p>This claim assumes one is using something like Bagwell’s hash array mapped tries for structural sharing. <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Finn Völkel</name></author><summary type="html"><![CDATA[Edit(2026-04-30): After having thought some more about this post there a multiple issues. The algorithm is not correct and the claim about WCOJ needs to be more precise. There are multiple definitions for worst-case optimality in the context of IVM and I need to get them straight before actually claiming anything. The more serious issue is correctness. Let $P_{i}$ be the set of prefixes at level $i$ of the algorithm below and $E_{i}$ be the set of extensions, then]]></summary></entry><entry><title type="html">WCOJ - DBSP, ZSets and Datalog</title><link href="https://finnvolkel.com/wcoj-dbsp-zsets-and-datalog" rel="alternate" type="text/html" title="WCOJ - DBSP, ZSets and Datalog" /><published>2026-01-02T00:00:00+00:00</published><updated>2026-01-02T00:00:00+00:00</updated><id>https://finnvolkel.com/wcoj-dbsp-zsets-and-datalog</id><content type="html" xml:base="https://finnvolkel.com/wcoj-dbsp-zsets-and-datalog"><![CDATA[<p>Edit(2026-04-30): There are some issues with the complexity claims below. I am working on some udates to the WCOJ series. Also see the edit at the top of <a href="wcoj-wcoj-meets-dbsp">WCOJ - WCOJ meets DBSP</a>.</p>

<p><em>Tying together DBSP and updates in a Datalog engine in preparation for a WCOJ for DBSP</em></p>

<p>The folks from <a href="https://www.feldera.com/">Feldera</a> have written some good blog posts on <a href="https://www.feldera.com/blog/z-sets-representing-database-changes">Z-sets</a> and <a href="https://www.feldera.com/blog/implementing-z-sets">their</a> <a href="https://www.feldera.com/blog/implementing-z-sets">implementation</a>. Reading them will make what follows a lot easier (but it’s hopefully not a requirement). I am also going to use some notation from their DBSP paper <sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">1</a></sup>.</p>

<p>In previous posts (<a href="wcoj-graph-join-correspondence">1</a>, <a href="wcoj-generic-join">2</a>, <a href="wcoj-datalog-and-genericjoin">3</a>) we saw the implementation of a WCOJ algorithm called GenericJoin and how parts of the <a href="https://v1-docs.xtdb.com/language-reference/1.24.3/datalog-queries/">EDN Datalog</a> query language could be implemented via concrete instantiations of the GenericJoin <code class="language-plaintext highlighter-rouge">PrefixExtender</code> interface.  In this post we are going to do a detour to <a href="https://arxiv.org/abs/2203.16684">DBSP</a>. DBSP is an incremental view maintenance framework. Given a database $DB$ and a query $Q$, the incremental view maintenance (IVM) problem concerns itself with calculating the changes to the query $\Delta Q$ by only processing the changes (inserts, updates and deletes) $\Delta DB$ to the database.
DBSP gives a framework to incrementalize all of relational algebra and <a href="https://en.wikipedia.org/wiki/Datalog">stratified datalog</a>. Datomic-style Datalog engines are essentially stratified Datalog, as negation clauses are only evaluated when all variables in the negated part are bound. This means given a SQL or Datalog query $Q$, DBSP gives a method to calculate updates to the query efficiently.</p>
<h3 id="efficiency">Efficiency</h3>
<p>So, given database $DB$ and some changes $\Delta DB$ (I just think of this as a transaction) to the database. An incremental version of a query outputs the changes to the query through transaction history. Most of the time I will just be using query to mean the incremental version of the query (also sometimes called a streaming query). A standard database query works on the database snapshots. Streaming queries work on a stream of changes/transaction to the database. From context it should be clear of which query type I am writing about. In the DBSP paper the standard and streaming query are distinguished by $S$ and $S^\Delta$, but I will just drop the superscript for now. A simple example for the streaming version of a query would be, given the query</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">person</span><span class="w"> </span><span class="nb">name</span><span class="p">]</span><span class="w">
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[</span><span class="n">person</span><span class="w"> </span><span class="no">:name</span><span class="w"> </span><span class="nb">name</span><span class="p">]}</span><span class="w">
</span></code></pre></div></div>
<p>and the transaction</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[[</span><span class="no">:db/add</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="no">:name</span><span class="w"> </span><span class="s">"Ada Lovelace"</span><span class="p">]]</span><span class="w">
</span></code></pre></div></div>
<p>one would obtain the result (assuming Ada Lovelace is not yet part of the db)</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">#</span><span class="p">{[[</span><span class="mi">1</span><span class="w"> </span><span class="s">"Ada Lovelace"</span><span class="p">]</span><span class="w"> </span><span class="mi">1</span><span class="p">]}</span><span class="w">
</span></code></pre></div></div>
<p>The entity id 1 for a person with the name “Ada Lovelace” were added to the database with a multiplicity of 1.</p>

<p>By efficient I mean that for most cases these changes can be calculated without reevaluating the whole query from scratch. The assumption always being that $|\Delta DB| \ll |DB|$. For many linear operators like <code class="language-plaintext highlighter-rouge">filter</code>, <code class="language-plaintext highlighter-rouge">map</code> and <code class="language-plaintext highlighter-rouge">project</code> this is $O(|\Delta DB|)$.<br />
As the following example shows this bound doesn’t hold in the triangle query case (for joins). A single edge deletion can trigger a $O(n)$ output changes.</p>

<p><img src="/assets/triangle_ivm_problme_graph.png" alt="triangle_ivm_problme_graph" />
The edge deletion of $(a,b)$ removes all triangles from the graph.
We can do even worse (argh !). Consider the following graph 
<img src="/assets/wcoj_ivm_worst_case.png" alt="wcoj_ivm_worst_case" />
from the beloved triangle query perspective.</p>

\[Q(x,y,z) = R(x,y) \bowtie S(y,z) \bowtie T(x,z)\]

<p>with the setup of</p>

\[\begin{align*}
R &amp;= \{(1, 1), (1, 3), (1, 5), \ldots, (1, 2n-1)\} \\
S &amp;= \{(2, 1), (4, 1), (6, 1), \ldots, (2n, 1)\} \\
T &amp;= \emptyset
\end{align*}\]

<p>and the variable join order of $(x,y,z)$. When inserting the tuple $T(1,1)$ no triangles get created, so there are no output changes, but we still need to intersect $R$ and $S$ on $y$. This intersection is empty, but the intersection will always take $O(n)$ even with a WCOJ algorithm.</p>

<p>The important part is that joins in DBSP are still more efficient then doing a naive reevaluation of the join, mainly  $O(|DB| × |\Delta DB|)$ instead of $O(|DB|^2)$.</p>

<p>Similarly for recursive queries. Let’s say you calculate the transitive closure of a graph (using <code class="language-plaintext highlighter-rouge">[from :g/to to]</code> triples as in our <a href="wcoj-graph-join-correspondence">first post</a>).</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w"> </span><span class="o">'</span><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">?a</span><span class="w"> </span><span class="n">?b</span><span class="p">]</span><span class="w">
   </span><span class="no">:where</span><span class="w"> </span><span class="p">[(</span><span class="nf">reachable</span><span class="w"> </span><span class="n">?a</span><span class="w"> </span><span class="n">?b</span><span class="p">)]</span><span class="w"> 
   </span><span class="no">:rules</span><span class="w"> </span><span class="p">[[(</span><span class="nf">reachable</span><span class="w"> </span><span class="n">?a</span><span class="w"> </span><span class="n">?b</span><span class="p">)</span><span class="w">
	        </span><span class="p">[</span><span class="n">?a</span><span class="w"> </span><span class="no">:g/to</span><span class="w"> </span><span class="n">?b</span><span class="p">]]</span><span class="w">
           </span><span class="p">[(</span><span class="nf">reachable</span><span class="w"> </span><span class="n">?a</span><span class="w"> </span><span class="n">?b</span><span class="p">)</span><span class="w">
            </span><span class="p">[</span><span class="n">?a</span><span class="w"> </span><span class="no">:g/to</span><span class="w"> </span><span class="n">?c</span><span class="p">]</span><span class="w">
            </span><span class="p">(</span><span class="nf">reachable</span><span class="w"> </span><span class="n">?c</span><span class="w"> </span><span class="n">?b</span><span class="p">)]]]})</span><span class="w">
</span></code></pre></div></div>
<p>The calculation involved in finding the transitive closure is finding a fixpoint where reachable states don’t grow anymore.  Any addition or retraction of an edge could trigger the recalculation of that fixpoint and potentially has consequences that propagate to all output tuples, meaning the work is also no longer bound by  $O(|\Delta DB |)$, but rather some polynomial in $|DB|$ which is of course not efficient in the strictest sense. DBSP will in most cases still perform better than an naive version ($O(|DB| |\Delta DB|)$ vs $O(|DB| ^{2})$ for the transitive closure).</p>

<p>In what follows I will use <a href="https://en.wikipedia.org/wiki/Clojure#Extensible_Data_Notation">EDN</a> notation for maps.  For example</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">;; simple map</span><span class="w">
</span><span class="p">{</span><span class="n">key1</span><span class="w"> </span><span class="n">value1,</span><span class="w"> </span><span class="n">key2</span><span class="w"> </span><span class="n">value2</span><span class="p">}</span><span class="w">
</span><span class="c1">;; nested map</span><span class="w">
</span><span class="p">{</span><span class="n">key1</span><span class="w"> </span><span class="p">{</span><span class="n">nested-key1</span><span class="w"> </span><span class="n">value1,</span><span class="w"> </span><span class="n">nested-key2</span><span class="w"> </span><span class="n">value2</span><span class="p">}</span><span class="n">,</span><span class="w"> </span><span class="n">...</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h3 id="z-sets">Z-Sets</h3>
<p>Z-Sets are the fundamental building block of DBSP so I want to give some intuition of how they work. Z-Sets are a generalisation of sets and bags. Every element in the set has a multiplicity and the multiplicity might be negative. This allows to represent 2 kinds of data in a database system:</p>
<ul>
  <li>The state of a relation/table/index</li>
  <li>The changes to a relation by a transaction
A transaction produces changes in a database system. For additive changes the weights in the Z-set representation are positive and for deletions they are negative.</li>
</ul>

<p>Z-Sets are not maps per se, as they are considered sets, but fundamentally they are backed by maps (at least in a simple implementation, see <a href="https://www.feldera.com/blog/implementing-z-sets">implementing Z-sets</a> for an example), so I am going to abuse the map notation from above a bit and write Z-sets as maps.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="s">"Alice"</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="s">"Bob"</span><span class="w"> </span><span class="mi">-3</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>This map would mean that element <code class="language-plaintext highlighter-rouge">Alice</code> has a multiplicity of 1 and that <code class="language-plaintext highlighter-rouge">Bob</code> has a multiplicity of -3. Z-sets have the nice property that they form an abelian group (don’t worry if you don’t know what that means). You can add and subtract Z-sets like normal sets and they behave nicely. For example</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="s">"Alice"</span><span class="w"> </span><span class="mi">1</span><span class="n">,</span><span class="w"> </span><span class="s">"Bob"</span><span class="w"> </span><span class="mi">-3</span><span class="p">}</span><span class="w">
</span><span class="nb">+</span><span class="w">
</span><span class="p">{</span><span class="s">"Eve"</span><span class="w"> </span><span class="mi">-1</span><span class="w"> </span><span class="s">"Bob"</span><span class="w"> </span><span class="mi">2</span><span class="p">}</span><span class="w">
</span><span class="c1">;; =&gt;</span><span class="w">
</span><span class="p">{</span><span class="s">"Alice"</span><span class="w"> </span><span class="mi">1</span><span class="n">,</span><span class="w"> </span><span class="s">"Bob"</span><span class="w"> </span><span class="mi">-1</span><span class="n">,</span><span class="w"> </span><span class="s">"Eve"</span><span class="w"> </span><span class="mi">-1</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>This operation can be implemented efficiently on Z-sets. You can also define the negation of a Z-set which negates all weights. The empty Z-set behaves like 0 for addition and subtraction. Scalar multiplication of a Z-set multiplies all weights by the scalar. All of this means Z-sets have nice mathematical properties.</p>

<p>An <code class="language-plaintext highlighter-rouge">equi-join</code> of two Z-sets is multiplying the weights of two matching keys (non matching keys can be thought of as having a counter-part of multiplicity 0).</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="s">"Alice"</span><span class="w"> </span><span class="mi">1</span><span class="n">,</span><span class="w"> </span><span class="s">"Bob"</span><span class="w"> </span><span class="mi">-3</span><span class="p">}</span><span class="w">
</span><span class="nb">join</span><span class="w">
</span><span class="p">{</span><span class="s">"Alice"</span><span class="w"> </span><span class="mi">-1</span><span class="n">,</span><span class="w"> </span><span class="s">"Eve"</span><span class="w"> </span><span class="mi">-1</span><span class="w"> </span><span class="s">"Bob"</span><span class="w"> </span><span class="mi">2</span><span class="p">}</span><span class="w">
</span><span class="c1">;; =&gt;</span><span class="w">
</span><span class="p">{</span><span class="s">"Alice"</span><span class="w"> </span><span class="mi">-1</span><span class="n">,</span><span class="w"> </span><span class="s">"Bob"</span><span class="w"> </span><span class="mi">-6</span><span class="p">}</span><span class="w"> 
</span></code></pre></div></div>
<h3 id="indexed-z-sets">Indexed Z-Sets</h3>
<p>Indexed Z-sets extend Z-sets by adding structure that enables efficient lookups and grouping. An indexed Z-set is essentially a map from keys to Z-sets of values. You can think of it as grouping elements by some key, where each key maps to a Z-set of associated values with their multiplicities.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:accounting</span><span class="w"> </span><span class="p">{</span><span class="s">"Alice"</span><span class="w"> </span><span class="mi">1</span><span class="n">,</span><span class="w"> </span><span class="s">"Bob"</span><span class="w"> </span><span class="mi">2</span><span class="p">}</span><span class="w">
 </span><span class="no">:engineering</span><span class="w"> </span><span class="p">{</span><span class="s">"Eve"</span><span class="w"> </span><span class="mi">1</span><span class="n">,</span><span class="w"> </span><span class="s">"Charlie"</span><span class="w"> </span><span class="mi">-1</span><span class="p">}}</span><span class="w">
</span></code></pre></div></div>
<p>This indexed Z-set groups people by department. The outer structure is the index (department), and each value is itself a Z-set of people with their multiplicities.
Indexed Z-sets still form an abelian group. You add them by merging keys and adding the inner Z-sets pointwise:</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:accounting</span><span class="w"> </span><span class="p">{</span><span class="s">"Alice"</span><span class="w"> </span><span class="mi">1</span><span class="p">}</span><span class="n">,</span><span class="w"> </span><span class="no">:engineering</span><span class="w"> </span><span class="p">{</span><span class="s">"Eve"</span><span class="w"> </span><span class="mi">2</span><span class="p">}}</span><span class="w">
</span><span class="nb">+</span><span class="w">
</span><span class="p">{</span><span class="no">:accounting</span><span class="w"> </span><span class="p">{</span><span class="s">"Bob"</span><span class="w"> </span><span class="mi">1</span><span class="p">}</span><span class="n">,</span><span class="w"> </span><span class="no">:engineering</span><span class="w"> </span><span class="p">{</span><span class="s">"Eve"</span><span class="w"> </span><span class="mi">-1</span><span class="p">}}</span><span class="w">
</span><span class="c1">;; =&gt;</span><span class="w">
</span><span class="p">{</span><span class="no">:accounting</span><span class="w"> </span><span class="p">{</span><span class="s">"Alice"</span><span class="w"> </span><span class="mi">1</span><span class="n">,</span><span class="w"> </span><span class="s">"Bob"</span><span class="w"> </span><span class="mi">1</span><span class="p">}</span><span class="n">,</span><span class="w"> </span><span class="no">:engineering</span><span class="w"> </span><span class="p">{</span><span class="s">"Eve"</span><span class="w"> </span><span class="mi">1</span><span class="p">}}</span><span class="w">
</span></code></pre></div></div>
<p>Aggregation over indexed Z-sets works by applying a function to each inner Z-set. For example, summing the multiplicities within each group:</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:accounting</span><span class="w"> </span><span class="p">{</span><span class="s">"Alice"</span><span class="w"> </span><span class="mi">1</span><span class="n">,</span><span class="w"> </span><span class="s">"Bob"</span><span class="w"> </span><span class="mi">2</span><span class="p">}</span><span class="n">,</span><span class="w"> </span><span class="no">:engineering</span><span class="w"> </span><span class="p">{</span><span class="s">"Eve"</span><span class="w"> </span><span class="mi">1</span><span class="p">}}</span><span class="w">
</span><span class="c1">;; aggregate (sum multiplicities) =&gt;</span><span class="w">
</span><span class="p">{</span><span class="no">:accounting</span><span class="w"> </span><span class="mi">3</span><span class="n">,</span><span class="w"> </span><span class="no">:engineering</span><span class="w"> </span><span class="mi">1</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Linear aggregates like <code class="language-plaintext highlighter-rouge">sum</code> and <code class="language-plaintext highlighter-rouge">count</code> incrementalize cleanly. Non-linear aggregates like <code class="language-plaintext highlighter-rouge">min</code>, <code class="language-plaintext highlighter-rouge">max</code>, and <code class="language-plaintext highlighter-rouge">distinct count</code> require more sophisticated treatment (often involving auxiliary state) to maintain incrementally.</p>
<h3 id="updates-to-the-eav-aev-and-vae--indices">Updates to the <code class="language-plaintext highlighter-rouge">EAV</code>, <code class="language-plaintext highlighter-rouge">AEV</code> and <code class="language-plaintext highlighter-rouge">VAE</code>  indices</h3>
<p>If you have not been following along AEV and VAE are permutations of the primary <a href="https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model">entity-attribute-value</a> (EAV) index, which is the backbone of the toy-Datalog engine<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">2</a></sup> we have been considering along these posts.
As we have seen above, any deletion or addition in Datalog can be modelled in terms of Z-Sets. So for example the transaction</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[[</span><span class="no">:db/add</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="no">:name</span><span class="w"> </span><span class="s">"Ada Lovelace"</span><span class="p">]</span><span class="w">
 </span><span class="p">[</span><span class="no">:db/rectract</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="no">:name</span><span class="w"> </span><span class="s">"Alan Turing"</span><span class="p">]]</span><span class="w">
</span></code></pre></div></div>
<p>can be modelled as a Z-Set of the form</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{[</span><span class="mi">1</span><span class="w"> </span><span class="no">:name</span><span class="w"> </span><span class="s">"Ada Lovelace"</span><span class="p">]</span><span class="w"> </span><span class="mi">1</span><span class="w">
 </span><span class="p">[</span><span class="mi">2</span><span class="w"> </span><span class="no">:name</span><span class="w"> </span><span class="s">"Alan Turing"</span><span class="p">]</span><span class="w">  </span><span class="mi">-1</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>We can also model these updates as nested indexed Z-Sets. So instead of having the full row as the key of our Z-Set we have the entity id on the first level, the attribute on the second and the values on the final level. For the transaction above and the <code class="language-plaintext highlighter-rouge">EAV</code> index this results in the following nested indexed Z-set.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="mi">1</span><span class="w"> </span><span class="p">{</span><span class="no">:name</span><span class="w"> </span><span class="p">{</span><span class="s">"Ada Lovelace"</span><span class="w"> </span><span class="mi">1</span><span class="p">}}</span><span class="w">
 </span><span class="mi">2</span><span class="w"> </span><span class="p">{</span><span class="no">:name</span><span class="w"> </span><span class="p">{</span><span class="s">"Alan Turing"</span><span class="w"> </span><span class="mi">-1</span><span class="p">}}}</span><span class="w">
</span></code></pre></div></div>
<p>For the <code class="language-plaintext highlighter-rouge">AEV</code> and <code class="language-plaintext highlighter-rouge">VAE</code> indices you get similar indexed Z-sets with the entity, attribute and value permutated accordingly. I am not sure if indexed Z-sets were supposed to be used in DBSP in this manner. One could also imagine a standard Z-set but backed by some kind of opaque trie underneath. The important part is that this representation allows us to efficiently look up changes by a prefix.</p>
<h3 id="efficient-multi-joins-in-dbsp">Efficient multi-joins in DBSP</h3>
<p>DBSP defines its operators over streams, an infinite sequences of Z-sets indexed by time. However, at each tick the actual computation works with Z-sets: the current deltas arriving and the accumulated state from previous ticks (not every operator needs the previous state, but some do). In what follows, I’ll write $A^\Delta$ for the delta arriving at the current tick and $A^{-1}$ for the accumulated state just before that current delta is applied. Both are Z-sets. The formulas hold at every tick. I’ll leave the time index implicit. I am also using the notation $R^A$ to denote relation $R$ restricted to variable $A$. This is to disambiguate something like $R_1^A \bowtie R_2^A$ from $R_1 \bowtie R_2$ when $R_1$ and $R_2$ share more than one join variable.</p>

<p>With this notation, the incremental join is given by the bilinear formula:</p>

\[% Full theorem statement:
(A \bowtie B)^\Delta = A^\Delta \bowtie B^\Delta + A^{-1} \bowtie B^\Delta + A^\Delta \bowtie B^{-1}\]

<p>The output delta is computed from three terms: the join of the two input deltas, and two “cross terms” joining each delta with the other input’s previous state. Expanding out $(A \bowtie B \bowtie C)^\Delta$ already gives you 7 terms. 
So this won’t scale well if you do multi-joins naively. Let’s say we want to compute</p>

\[(R_1^A \bowtie R_2^A \ \dots \ \bowtie R_k^A)^\Delta\]

<p>using the binary join forumla above we can obtain the following recursive formula to calculate the full result.</p>

\[\begin{align*}
\Delta_{1..i} &amp;= \Delta_{1..i-1} \bowtie \Delta R_i^A \\
              &amp;+ \Delta_{1..i-1} \bowtie (R_i^A)^{-1} \\
              &amp;+ \Delta R_i^A \bowtie (R_1^A)^{-1} \bowtie \dots \bowtie (R_{i-1}^A)^{-1}
\end{align*}\]

<p>with $\Delta_{1..i}$ being the result for the first $i$ deltas. You can obtain this formula by applying the standard join formula on $\Delta_{1..{i-1}}$  and $R_i^A$. 
Going from $\Delta_{1..{i-1}}$ to $\Delta_{1..i}$ means joining the previous delta accumulation $\Delta_{1..i-1}$ with  the delta under consideration $\Delta R_i^A$ , the previous accumulation with the delayed relations $(R_i^A)^{-1}$ and the delta under consideration with all the delayed relations indexed by less than $i$. Notice that for $i=2$ this is the standard DBSP join formula. The left side of all three terms of the formula is a delta, meaning the joins at each iteration are bounded by  $O(|\Delta DB||DB|)$, hence the the whole process is bounded by $O(|\Delta DB||DB|)$ for a fixed query. We will be use this join formula when implementing the variable join (similar to the <code class="language-plaintext highlighter-rouge">GenericSingleJoin</code> from the <a href="wcoj-generic-join">second post in the series</a>) by level for a GenericJoin-like implementation in DBSP.</p>
<h3 id="full-database-history-in-support-of-dbsp">Full database history in support of DBSP</h3>
<p>In DBSP you (sometimes) need to store state history for the DBSP circuits to work correctly. Looking at the join operator formula from above once more:</p>

\[% Full theorem statement:
(A \bowtie B)^\Delta = A^\Delta \bowtie B^\Delta + A^{-1} \bowtie B^\Delta + A^\Delta \bowtie B^{-1}\]

<p>$A^{-1}$ is the full relation just before applying the current delta. One insight I had while writing this post was that databases that have full history give you the previous state essentially for free. This assumes $A$ and $B$ being base relations (one of the <code class="language-plaintext highlighter-rouge">EAV</code> indexes for Datomic-like engines). If $A$ or $B$ are derived relations this is not possible as there is likely state involved that can not be easily obtained via a view over a database snapshot, think for example of a function application. For base relations (indexes), you can obtain the appropriate Z-sets by simply™ creating a view over the appropriate index at the given timestamp.</p>

<p>If we once again consider the query from above</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">person</span><span class="w"> </span><span class="nb">name</span><span class="p">]</span><span class="w">
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[</span><span class="n">person</span><span class="w"> </span><span class="no">:name</span><span class="w"> </span><span class="nb">name</span><span class="p">]}</span><span class="w">
</span></code></pre></div></div>
<p>the lookup of these two variables will likely happen on the <code class="language-plaintext highlighter-rouge">AEV</code> index. So $AEV^{-1}$  doesn’t need to be stored inside the operators, one can simply provide a Z-set view over the index and filter out triples from newer transactions.</p>

<p>The cool thing is that databases that <a href="https://docs.datomic.com/peer-tutorial/see-historic-data.html">support</a> <a href="https://xtdb.com/">history</a> could let you run an incremental query from a previous snapshot. Instead of writing queries over your history (via some special syntax), you simply write your query, initialise it from a given snapshot and then replay the transaction log.</p>

<p>Up <a href="wcoj-wcoj-meets-dbsp">next</a> we will see how to put the concepts of this post together with <a href="wcoj-generic-join">Generic Join</a> to obtain an efficient version of a WCOJ algorithm in DBSP.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://arxiv.org/abs/2203.16684">https://arxiv.org/abs/2203.16684</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p><a href="https://github.com/fiV0/hooray2">https://github.com/fiV0/hooray2</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Finn Völkel</name></author><summary type="html"><![CDATA[Edit(2026-04-30): There are some issues with the complexity claims below. I am working on some udates to the WCOJ series. Also see the edit at the top of WCOJ - WCOJ meets DBSP.]]></summary></entry><entry><title type="html">WCOJ - Datalog and GenericJoin</title><link href="https://finnvolkel.com/wcoj-datalog-and-genericjoin" rel="alternate" type="text/html" title="WCOJ - Datalog and GenericJoin" /><published>2025-12-15T00:00:00+00:00</published><updated>2025-12-15T00:00:00+00:00</updated><id>https://finnvolkel.com/wcoj-datalog-and-genericjoin</id><content type="html" xml:base="https://finnvolkel.com/wcoj-datalog-and-genericjoin"><![CDATA[<p>Edit(07-06-2026): I have realized that the <code class="language-plaintext highlighter-rouge">OrPrefixExtender</code> below is not quite right, as variable instantiations from other <code class="language-plaintext highlighter-rouge">or</code> branches can “leak” into extensions in lower variable levels. Or branches need to get executed in isolation, at least for the variables they participate in. The other option is to check the branch again when it gets extended for a particular prefix, but that seems expensive.</p>

<p><em>See how easy it is to extend the GenericJoin interface <code class="language-plaintext highlighter-rouge">PrefixExtender</code> for <code class="language-plaintext highlighter-rouge">or</code>, <code class="language-plaintext highlighter-rouge">and</code>and<code class="language-plaintext highlighter-rouge">not</code></em></p>

<p>Recapping from <a href="wcoj-generic-join">our last post</a>, the interface implemented for a data pattern clause backed by one of the indexes  (some permutation of an EAV index) is</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">PrefixExtender</span> <span class="p">:</span> <span class="nc">LevelParticipation</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">count</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">):</span> <span class="nc">Int</span>
    <span class="k">fun</span> <span class="nf">propose</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">)</span> <span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;</span>
    <span class="k">fun</span> <span class="nf">intersect</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">,</span> <span class="n">extensions</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;)</span> <span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>So far we have assumed any clause is just a standard data pattern clause  <code class="language-plaintext highlighter-rouge">[entity attribute variable]</code> which can be directly matched against one of the <code class="language-plaintext highlighter-rouge">EAV</code>, <code class="language-plaintext highlighter-rouge">AEV</code> or <code class="language-plaintext highlighter-rouge">VAE</code> indexes, but Datalog is more expressive than that.</p>
<h3 id="or">Or</h3>
<p>With an <code class="language-plaintext highlighter-rouge">or</code> clause you can express that logic variables satisfy one or more clauses. So for example the following query finds people that have a last name of <code class="language-plaintext highlighter-rouge">Lovelace</code> and either have a first-name of <code class="language-plaintext highlighter-rouge">Ada</code> <em>or</em> are of the gender male. The restriction on <code class="language-plaintext highlighter-rouge">or</code> is that the set of variables appearing in the different child clauses must be identical.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">p</span><span class="p">]</span><span class="w">
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[[</span><span class="n">p</span><span class="w"> </span><span class="no">:last-name</span><span class="w"> </span><span class="s">"Lovelace"</span><span class="p">]</span><span class="w">
         </span><span class="p">(</span><span class="nb">or</span><span class="w"> </span><span class="p">[</span><span class="n">p</span><span class="w"> </span><span class="no">:first-name</span><span class="w"> </span><span class="s">"Ada"</span><span class="p">]</span><span class="w"> 
             </span><span class="p">[</span><span class="n">p</span><span class="w"> </span><span class="no">:gender</span><span class="w"> </span><span class="no">:male</span><span class="p">])]}</span><span class="w">
</span></code></pre></div></div>
<p>A <code class="language-plaintext highlighter-rouge">GenericOrPrefixExtender</code><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> can be recursively defined on nested <code class="language-plaintext highlighter-rouge">PrefixExtender</code>s. The logic mainly derives from the fact that an <code class="language-plaintext highlighter-rouge">or</code> is the union of all its sub-clauses.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">open</span> <span class="kd">class</span> <span class="nc">GenericOrPrefixExtender</span><span class="p">(</span><span class="kd">val</span> <span class="py">children</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">PrefixExtender</span><span class="p">&gt;)</span> <span class="p">:</span> <span class="nc">PrefixExtender</span> <span class="p">{</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">count</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">)</span> <span class="p">=</span> <span class="n">children</span><span class="p">.</span><span class="nf">sumOf</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">count</span><span class="p">(</span><span class="n">prefix</span><span class="p">)</span> <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">propose</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">)</span> <span class="p">=</span> <span class="n">children</span><span class="p">.</span><span class="nf">flatMap</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">propose</span><span class="p">(</span><span class="n">prefix</span><span class="p">)</span> <span class="p">}.</span><span class="nf">distinct</span><span class="p">()</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">intersect</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">,</span> <span class="n">extensions</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;):</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">result</span> <span class="p">=</span> <span class="n">mutableListOf</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;()</span>
        <span class="k">for</span> <span class="p">(</span><span class="n">child</span> <span class="k">in</span> <span class="n">children</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">childExtensions</span> <span class="p">=</span> <span class="n">child</span><span class="p">.</span><span class="nf">intersect</span><span class="p">(</span><span class="n">prefix</span><span class="p">,</span> <span class="n">extensions</span><span class="p">)</span>
            <span class="n">result</span><span class="p">.</span><span class="nf">addAll</span><span class="p">(</span><span class="n">childExtensions</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">result</span><span class="p">.</span><span class="nf">distinct</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="c1">// All or clauses have the same variables, hence participate in the same levels</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">participatesInLevel</span><span class="p">(</span><span class="n">level</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">=</span> <span class="n">children</span><span class="p">.</span><span class="nf">first</span><span class="p">().</span><span class="nf">participatesInLevel</span><span class="p">(</span><span class="n">level</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>In the extreme case all children propose different extensions so <code class="language-plaintext highlighter-rouge">count</code> is the sum of its children counts. For <code class="language-plaintext highlighter-rouge">propose</code> we take the proposals from all its children and assure we are not proposing candidates twice (<code class="language-plaintext highlighter-rouge">distinct</code>). Similarly for <code class="language-plaintext highlighter-rouge">intersect</code>, we intersect the given extensions with all the child clauses and assure uniqueness with <code class="language-plaintext highlighter-rouge">distinct</code>. The <code class="language-plaintext highlighter-rouge">distinct</code> logic could likely be better optimised as the implementation may currently create large intermediate lists. For example if extensions are sorted one could do a two-way merge for the set intersection. The complexity of the WCOJ is not affected by leaving this optimisation on the table.</p>
<h3 id="and">And</h3>
<p>At the toplevel (and other logical connectors) conjunction is the default, so you don’t need an explicit <code class="language-plaintext highlighter-rouge">and</code> clause, but you may need so inside an <code class="language-plaintext highlighter-rouge">or</code> clause.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">p</span><span class="p">]</span><span class="w">
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[[</span><span class="n">p</span><span class="w"> </span><span class="no">:last-name</span><span class="w"> </span><span class="s">"Lovelace"</span><span class="p">]</span><span class="w">
         </span><span class="p">(</span><span class="nb">or</span><span class="w"> </span><span class="p">[</span><span class="n">p</span><span class="w"> </span><span class="no">:first-name</span><span class="w"> </span><span class="s">"Ada"</span><span class="p">]</span><span class="w"> 
             </span><span class="p">(</span><span class="nb">and</span><span class="w"> </span><span class="p">[</span><span class="n">p</span><span class="w"> </span><span class="no">:first-name</span><span class="w"> </span><span class="s">"Alan"</span><span class="p">]</span><span class="w"> 
                  </span><span class="p">[</span><span class="n">p</span><span class="w"> </span><span class="no">:gender</span><span class="w"> </span><span class="no">:male</span><span class="p">]))]}</span><span class="w">
</span></code></pre></div></div>
<p>The above query finds people whose last name is “Lovelace” and who either have a first name of <code class="language-plaintext highlighter-rouge">Ada</code> <em>or</em> have first name of <code class="language-plaintext highlighter-rouge">Alan</code> <em>and</em> are also male.
The corresponding <code class="language-plaintext highlighter-rouge">GenericAndPrefixExtender</code></p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">open</span> <span class="kd">class</span> <span class="nc">GenericAndPrefixExtender</span><span class="p">(</span><span class="kd">val</span> <span class="py">children</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">PrefixExtender</span><span class="p">&gt;)</span> <span class="p">:</span> <span class="nc">PrefixExtender</span> <span class="p">{</span>

    <span class="c1">// The count is the minimum of counts of all children</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">count</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">)</span> <span class="p">=</span> <span class="n">children</span><span class="p">.</span><span class="nf">minOf</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">count</span><span class="p">(</span><span class="n">prefix</span><span class="p">)</span> <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">propose</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">)</span> <span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">nextLevel</span> <span class="p">=</span>  <span class="n">prefix</span><span class="p">.</span><span class="n">size</span>
        <span class="kd">val</span> <span class="py">participants</span> <span class="p">=</span> <span class="n">children</span><span class="p">.</span><span class="nf">filter</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">participatesInLevel</span><span class="p">(</span><span class="n">nextLevel</span><span class="p">)</span> <span class="p">}</span>
        <span class="kd">val</span> <span class="py">minChild</span> <span class="p">=</span> <span class="n">participants</span><span class="p">.</span><span class="nf">minBy</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">count</span><span class="p">(</span><span class="n">prefix</span><span class="p">)</span> <span class="p">}</span>
        <span class="kd">var</span> <span class="py">extensions</span> <span class="p">=</span> <span class="n">minChild</span><span class="p">.</span><span class="nf">propose</span><span class="p">(</span><span class="n">prefix</span><span class="p">)</span>
        <span class="k">for</span> <span class="p">(</span><span class="n">child</span> <span class="k">in</span> <span class="n">participants</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">child</span> <span class="p">!=</span> <span class="n">minChild</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">extensions</span> <span class="p">=</span> <span class="n">child</span><span class="p">.</span><span class="nf">intersect</span><span class="p">(</span><span class="n">prefix</span><span class="p">,</span> <span class="n">extensions</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">extensions</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">intersect</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">,</span> <span class="n">extensions</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;):</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="py">currentExtensions</span> <span class="p">=</span> <span class="n">extensions</span>
        <span class="kd">val</span> <span class="py">nextLevel</span> <span class="p">=</span> <span class="n">prefix</span><span class="p">.</span><span class="n">size</span>
        <span class="c1">// We could also only find the minChild here, but I thought the almost constant sort here wouldn't hurt much </span>
        <span class="kd">val</span> <span class="py">participants</span> <span class="p">=</span> <span class="n">children</span><span class="p">.</span><span class="nf">filter</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">participatesInLevel</span><span class="p">(</span><span class="n">nextLevel</span><span class="p">)</span> <span class="p">}.</span><span class="nf">sortedBy</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">count</span><span class="p">(</span><span class="n">prefix</span><span class="p">)</span> <span class="p">}</span>
        <span class="k">for</span> <span class="p">(</span><span class="n">child</span> <span class="k">in</span> <span class="n">participants</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">currentExtensions</span> <span class="p">=</span> <span class="n">child</span><span class="p">.</span><span class="nf">intersect</span><span class="p">(</span><span class="n">prefix</span><span class="p">,</span> <span class="n">currentExtensions</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">currentExtensions</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">participatesInLevel</span><span class="p">(</span><span class="n">level</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">=</span> <span class="n">children</span><span class="p">.</span><span class="nf">any</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">participatesInLevel</span><span class="p">(</span><span class="n">level</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>In the <code class="language-plaintext highlighter-rouge">and</code> extender we are almost doing the same logic as in the <code class="language-plaintext highlighter-rouge">GenericSingleJoin</code> code from the <a href="wcoj-generic-join">previous post</a> which makes sense as conjunction is implicit at the top-level of <code class="language-plaintext highlighter-rouge">:where</code>. An upper bound for the <code class="language-plaintext highlighter-rouge">count</code> is the smallest <code class="language-plaintext highlighter-rouge">count</code> of one of its children. In <code class="language-plaintext highlighter-rouge">propose</code> and <code class="language-plaintext highlighter-rouge">intersect</code> we assure that we start the intersection from the smallest (in size) child, similarly to the general<code class="language-plaintext highlighter-rouge">GenericJoin</code> to guarantee worst-case optimality.</p>
<h3 id="not">Not</h3>
<p>The <code class="language-plaintext highlighter-rouge">not</code> clause is the <a href="https://en.wikipedia.org/wiki/Join_(relational_algebra)#Antijoin">anti-join</a> of Datalog. You can use it to remove tuples that match the positive part of the query.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">p</span><span class="p">]</span><span class="w">
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[[</span><span class="n">a</span><span class="w"> </span><span class="no">:last-name</span><span class="w"> </span><span class="s">"Lovelace"</span><span class="p">]</span><span class="w">
         </span><span class="p">[</span><span class="n">a</span><span class="w"> </span><span class="no">:first-name</span><span class="w"> </span><span class="s">"Ada"</span><span class="p">]</span><span class="w">
         </span><span class="p">[</span><span class="n">a</span><span class="w"> </span><span class="no">:gender</span><span class="w"> </span><span class="n">g</span><span class="p">]</span><span class="w">
         </span><span class="p">[</span><span class="n">p</span><span class="w"> </span><span class="no">:last-name</span><span class="w"> </span><span class="s">"Lovelace"</span><span class="p">]</span><span class="w">
         </span><span class="p">(</span><span class="nb">not</span><span class="w"> </span><span class="p">[</span><span class="n">p</span><span class="w"> </span><span class="no">:gender</span><span class="w"> </span><span class="n">g</span><span class="p">])]}</span><span class="w">
</span></code></pre></div></div>
<p>The above query finds people of the Lovelace family that have a different gender to <a href="https://en.wikipedia.org/wiki/Ada_Lovelace">Ada Lovelace</a> . In EDN Datalog all variables inside the <code class="language-plaintext highlighter-rouge">not</code> clause must be bound outside the <code class="language-plaintext highlighter-rouge">not</code> scope (formally this is known as <a href="https://en.wikipedia.org/wiki/Stratification_(mathematics)#In_mathematical_logic">stratified Datalog</a> and will become important later on). For example one cannot write something like</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">p</span><span class="p">]</span><span class="w">
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[(</span><span class="nb">not</span><span class="w"> </span><span class="p">[</span><span class="n">p</span><span class="w"> </span><span class="no">:profession</span><span class="w"> </span><span class="no">:programmer</span><span class="p">])]}</span><span class="w">
</span></code></pre></div></div>
<p>to find all non-programmers in the database.  For our current purposes this means that the following <code class="language-plaintext highlighter-rouge">GenericNotPrefixExtender</code> will (should) never <code class="language-plaintext highlighter-rouge">propose</code> extensions.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">GenericNotPrefixExtender</span><span class="p">(</span><span class="kd">val</span> <span class="py">children</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">PrefixExtender</span><span class="p">&gt;,</span> <span class="kd">val</span> <span class="py">level</span><span class="p">:</span> <span class="nc">Int</span><span class="p">):</span> <span class="nc">PrefixExtender</span> <span class="p">{</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">count</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">):</span> <span class="nc">Int</span> <span class="p">=</span> <span class="nc">Int</span><span class="p">.</span><span class="nc">MAX_VALUE</span>

    <span class="c1">// If propose is called on NOT it means that the variable was not bound outside of NOT</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">propose</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">):</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;</span> <span class="p">{</span>
       <span class="k">throw</span> <span class="nc">IllegalStateException</span><span class="p">(</span><span class="s">"Propose should not be called on NOT prefix extender"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">intersect</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">,</span> <span class="n">extensions</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;):</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">prefixAndExtensionsExtender</span> <span class="p">=</span> <span class="nc">PrefixExtender</span><span class="p">.</span><span class="nf">createPrefixAndExtensionsExtender</span><span class="p">(</span><span class="n">prefix</span><span class="p">,</span> <span class="n">extensions</span><span class="p">)</span>
        <span class="kd">val</span> <span class="py">extensionsToRemove</span><span class="p">:</span> <span class="nc">Set</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">GenericJoin</span><span class="p">(</span><span class="n">children</span> <span class="p">+</span> <span class="n">prefixAndExtensionsExtender</span><span class="p">,</span> <span class="n">prefix</span><span class="p">.</span><span class="n">size</span> <span class="p">+</span> <span class="mi">1</span><span class="p">).</span><span class="nf">join</span><span class="p">().</span><span class="nf">map</span> <span class="p">{</span> <span class="n">resultTuple</span> <span class="p">-&gt;</span> <span class="n">resultTuple</span><span class="p">.</span><span class="nf">last</span><span class="p">()</span> <span class="p">}.</span><span class="nf">toHashSet</span><span class="p">()</span>

        <span class="k">return</span> <span class="n">extensions</span><span class="p">.</span><span class="nf">filterNot</span> <span class="p">{</span> <span class="n">ext</span> <span class="p">-&gt;</span> <span class="n">extensionsToRemove</span><span class="p">.</span><span class="nf">contains</span><span class="p">(</span><span class="n">ext</span><span class="p">)</span> <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">participatesInLevel</span><span class="p">(</span><span class="n">level</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">=</span> <span class="k">this</span><span class="p">.</span><span class="n">level</span> <span class="p">==</span> <span class="n">level</span>
<span class="p">}</span>
</code></pre></div></div>
<p>By setting the <code class="language-plaintext highlighter-rouge">count</code> conceptually to $\infty$ (just <code class="language-plaintext highlighter-rouge">MAX_VALUE</code> in reality) we are assuring that <code class="language-plaintext highlighter-rouge">propose</code> will never get called. <code class="language-plaintext highlighter-rouge">propose</code> will throw if called. The <code class="language-plaintext highlighter-rouge">level</code> the extender participates in is the last level of the variables participating in the <code class="language-plaintext highlighter-rouge">not</code> clauses. So for the <code class="language-plaintext highlighter-rouge">not</code> example query above, assuming the variable order is (<code class="language-plaintext highlighter-rouge">a</code>, <code class="language-plaintext highlighter-rouge">g</code>, <code class="language-plaintext highlighter-rouge">p</code>) , the not prefix extender would only participate in the 3rd level (after <code class="language-plaintext highlighter-rouge">g</code> is bound and going through extensions of <code class="language-plaintext highlighter-rouge">p</code>). In <code class="language-plaintext highlighter-rouge">intersect</code> we are making recursively use of <code class="language-plaintext highlighter-rouge">GenericJoin</code>.  The strategy is to treat the <code class="language-plaintext highlighter-rouge">not</code> clause as an inner join that tells us what to exclude. We construct a synthetic extender (<code class="language-plaintext highlighter-rouge">createPrefixAndExtensionsExtender</code>) representing our current prefix and candidate extensions, join it with the negated clauses, and remove any extensions that matched. The synthetic extender constrains the inner join to the prefix values already built, proposing exactly one value at each prefix level and then offering candidate extensions at the final level. See the <a href="#appendix">Appendix</a> for the implementation of this synthetic extender.</p>

<p>Note that this is quite a naive implementation of an anti-join. In the inner <code class="language-plaintext highlighter-rouge">GenericJoin</code> we could for example restrict the prefixes to the variables actually participating in the <code class="language-plaintext highlighter-rouge">not</code> clauses as other variables won’t get constrained by anything in recursive join (only take prefixes of <code class="language-plaintext highlighter-rouge">g</code> and <code class="language-plaintext highlighter-rouge">p</code> in our example above).</p>

<p>Complexity-wise all these 3 implementations are doing work that is bound by the size of the query (the number of sub-clauses in <code class="language-plaintext highlighter-rouge">or</code>, <code class="language-plaintext highlighter-rouge">and</code> and <code class="language-plaintext highlighter-rouge">not</code>respectively ) which can be considered constant for a fixed query. The work done at the bottom, actual triple clauses, is then by courtesy of the previous post, worst-case optimal.</p>

<p>Up <a href="wcoj-dbsp-zsets-and-datalog">next</a> we will do a little detour into <a href="https://arxiv.org/abs/2203.16684">DBSP</a>, an incremental computation framework, that efficiently incrementalizes all fundamental operators in relational algebra. This will then set us up for our final boss post where DBSP meets WCOJ.</p>

<h3 id="appendix">Appendix</h3>

<p>This is the code for the synthetic prefix extender used to constrain the results in the inner join of the <code class="language-plaintext highlighter-rouge">GenericNotPrefixExtender</code>.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">PrefixExtender</span> <span class="p">:</span> <span class="nc">LevelParticipation</span> <span class="p">{</span>
    <span class="o">..</span><span class="p">.</span>
    
    <span class="k">companion</span> <span class="k">object</span> <span class="p">{</span>

        <span class="k">fun</span> <span class="nf">createPrefixAndExtensionsExtender</span><span class="p">(</span><span class="n">fixedPrefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">,</span> <span class="n">fixedExtensions</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;):</span> <span class="nc">PrefixExtender</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">extensionSet</span> <span class="p">=</span> <span class="n">fixedExtensions</span><span class="p">.</span><span class="nf">toHashSet</span><span class="p">()</span>
            <span class="k">return</span> <span class="k">object</span><span class="p">:</span> <span class="nc">PrefixExtender</span> <span class="p">{</span>
                <span class="k">private</span> <span class="k">fun</span> <span class="nf">isPrefixMatching</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">):</span> <span class="nc">Boolean</span> <span class="p">=</span>
                    <span class="n">prefix</span><span class="p">.</span><span class="n">size</span> <span class="p">&lt;=</span> <span class="n">fixedPrefix</span><span class="p">.</span><span class="n">size</span> <span class="p">&amp;&amp;</span> <span class="n">fixedPrefix</span><span class="p">.</span><span class="nf">take</span><span class="p">(</span><span class="n">prefix</span><span class="p">.</span><span class="n">size</span><span class="p">)</span> <span class="p">==</span> <span class="n">prefix</span>

                <span class="k">override</span> <span class="k">fun</span> <span class="nf">count</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">):</span> <span class="nc">Int</span> <span class="p">=</span>
                    <span class="k">if</span> <span class="p">(</span><span class="nf">isPrefixMatching</span><span class="p">(</span><span class="n">prefix</span><span class="p">))</span>
                        <span class="k">if</span> <span class="p">(</span><span class="n">prefix</span><span class="p">.</span><span class="n">size</span> <span class="p">&lt;</span> <span class="n">fixedPrefix</span><span class="p">.</span><span class="n">size</span><span class="p">)</span> <span class="mi">1</span> <span class="k">else</span> <span class="n">fixedExtensions</span><span class="p">.</span><span class="n">size</span>
                    <span class="k">else</span> <span class="mi">0</span>

                <span class="k">override</span> <span class="k">fun</span> <span class="nf">propose</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">):</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;</span> <span class="p">=</span>
                    <span class="k">if</span> <span class="p">(</span><span class="nf">isPrefixMatching</span><span class="p">(</span><span class="n">prefix</span><span class="p">))</span>
                        <span class="k">if</span> <span class="p">(</span><span class="n">prefix</span><span class="p">.</span><span class="n">size</span> <span class="p">&lt;</span> <span class="n">fixedPrefix</span><span class="p">.</span><span class="n">size</span><span class="p">)</span> <span class="nf">listOf</span><span class="p">(</span><span class="n">fixedPrefix</span><span class="p">[</span><span class="n">prefix</span><span class="p">.</span><span class="n">size</span><span class="p">])</span> <span class="k">else</span> <span class="n">fixedExtensions</span>
                    <span class="k">else</span>
                        <span class="nf">emptyList</span><span class="p">()</span>

                <span class="k">override</span> <span class="k">fun</span> <span class="nf">intersect</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">,</span> <span class="n">extensions</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;):</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;</span> <span class="p">=</span>
                    <span class="k">if</span> <span class="p">(</span><span class="nf">isPrefixMatching</span><span class="p">(</span><span class="n">prefix</span><span class="p">))</span>
                        <span class="k">if</span> <span class="p">(</span><span class="n">prefix</span><span class="p">.</span><span class="n">size</span> <span class="p">&lt;</span> <span class="n">fixedPrefix</span><span class="p">.</span><span class="n">size</span><span class="p">)</span>
                            <span class="k">if</span> <span class="p">(</span><span class="n">extensions</span><span class="p">.</span><span class="nf">contains</span><span class="p">(</span><span class="n">fixedPrefix</span><span class="p">[</span><span class="n">prefix</span><span class="p">.</span><span class="n">size</span><span class="p">]))</span>
                                <span class="nf">listOf</span><span class="p">(</span><span class="n">fixedPrefix</span><span class="p">[</span><span class="n">prefix</span><span class="p">.</span><span class="n">size</span><span class="p">])</span>
                            <span class="k">else</span>
                                <span class="nf">emptyList</span><span class="p">()</span>
                        <span class="k">else</span>
                            <span class="n">extensions</span><span class="p">.</span><span class="nf">filter</span> <span class="p">{</span> <span class="n">ext</span> <span class="p">-&gt;</span> <span class="n">extensionSet</span><span class="p">.</span><span class="nf">contains</span><span class="p">(</span><span class="n">ext</span><span class="p">)</span> <span class="p">}</span>
                    <span class="k">else</span>
                        <span class="nf">emptyList</span><span class="p">()</span>

                <span class="k">override</span> <span class="k">fun</span> <span class="nf">participatesInLevel</span><span class="p">(</span><span class="n">level</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">=</span> <span class="n">level</span> <span class="p">&lt;=</span> <span class="n">fixedPrefix</span><span class="p">.</span><span class="n">size</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>You can find the code of this post at <a href="https://github.com/FiV0/hooray2">https://github.com/FiV0/hooray2</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Finn Völkel</name></author><summary type="html"><![CDATA[Edit(07-06-2026): I have realized that the OrPrefixExtender below is not quite right, as variable instantiations from other or branches can “leak” into extensions in lower variable levels. Or branches need to get executed in isolation, at least for the variables they participate in. The other option is to check the branch again when it gets extended for a particular prefix, but that seems expensive.]]></summary></entry><entry><title type="html">WCOJ - Generic Join</title><link href="https://finnvolkel.com/wcoj-generic-join" rel="alternate" type="text/html" title="WCOJ - Generic Join" /><published>2025-12-11T00:00:00+00:00</published><updated>2025-12-11T00:00:00+00:00</updated><id>https://finnvolkel.com/wcoj-generic-join</id><content type="html" xml:base="https://finnvolkel.com/wcoj-generic-join"><![CDATA[<p><em>Explanation of a worst-case-optimal variable oriented join algorithm - GenericJoin <sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">1</a></sup></em></p>

<p>We discussed what Worst-case optimal join (WCOJ) is in a <a href="/wcoj-graph-join-correspondence">previous post</a>. Today we are going to try to convey the intuitions behind an actual WCOJ algorithm. Most of the code will be in Kotlin <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">2</a></sup>. A WCOJ is a join that is variable oriented instead of relation oriented. What do I mean by that? Coming back to our beloved triangle query (You really need to love this query as it will stay with us for quite a while).</p>

\[Q(A,B,C) = R(A,B) \bowtie S(B,C) \bowtie T(C,A)\]

<p>You can join by relation by doing standard binary joins $(R \bowtie S) \bowtie T$ and producing tuples <code class="language-plaintext highlighter-rouge">(a, b, c)</code> after $R \bowtie S$ and then only validate against $T$. As we have seen previously the intermediate result might be quite large compared to the final output size. In a variable oriented join you add variables one at a time. So in case of the triangle query you first start by adding the $a_i$ that match both $R$ and $T$ and then extend the prefixes of $(a_i)$ with matching $b_j$s to prefixes $(a_i \space b_j)$. This approach assumes that you have an index of <code class="language-plaintext highlighter-rouge">AB</code> for $R$, to efficiently look up $b$ given an $a$.</p>

<p>This is also why I quite like the triangle query in Datalog as an example when explaining WCOJ.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">?a</span><span class="w"> </span><span class="n">?b</span><span class="w"> </span><span class="n">?c</span><span class="p">]</span><span class="w">
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[[</span><span class="n">?a</span><span class="w"> </span><span class="no">:g/to</span><span class="w"> </span><span class="n">?b</span><span class="p">]</span><span class="w">
         </span><span class="p">[</span><span class="n">?a</span><span class="w"> </span><span class="no">:g/to</span><span class="w"> </span><span class="n">?c</span><span class="p">]</span><span class="w">
         </span><span class="p">[</span><span class="n">?b</span><span class="w"> </span><span class="no">:g/to</span><span class="w"> </span><span class="n">?c</span><span class="p">]]}</span><span class="w">
</span></code></pre></div></div>

<p>The variables are explicit and in <a href="https://www.datomic.com/">Datomic</a> and <a href="https://v1-docs.xtdb.com/main/">XTDB v1</a> the indices <code class="language-plaintext highlighter-rouge">EAV</code>, <code class="language-plaintext highlighter-rouge">AEV</code> and <code class="language-plaintext highlighter-rouge">AVE</code> are also by default part of the database. Prefix lookups (given a bound variable<code class="language-plaintext highlighter-rouge">?a</code> you can efficiently find <code class="language-plaintext highlighter-rouge">?b</code> via the <code class="language-plaintext highlighter-rouge">AEV</code> index) are therefore possible (for most scenarios).</p>

<p>The following interface is shamelessly stolen from a very good <a href="https://www.frankmcsherry.org/dataflow/relational/join/2015/04/11/genericjoin.html">blog post</a> on Generic Join.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typealias</span> <span class="nc">Prefix</span> <span class="p">=</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Any</span><span class="p">&gt;</span>
<span class="k">typealias</span> <span class="nc">Extension</span> <span class="p">=</span> <span class="nc">Any</span>

<span class="kd">interface</span> <span class="nc">PrefixExtender</span> <span class="p">:</span> <span class="nc">LevelParticipation</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">count</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">):</span> <span class="nc">Int</span>
    <span class="k">fun</span> <span class="nf">propose</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">)</span> <span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;</span>
    <span class="k">fun</span> <span class="nf">intersect</span><span class="p">(</span><span class="n">prefix</span><span class="p">:</span> <span class="nc">Prefix</span><span class="p">,</span> <span class="n">extensions</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;)</span> <span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Extension</span><span class="p">&gt;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>For any triple clause in the query (<code class="language-plaintext highlighter-rouge">[?a :g/to ?b]</code> for the triangle query) we have this interface backed by one of the <code class="language-plaintext highlighter-rouge">EAV</code>, <code class="language-plaintext highlighter-rouge">AEV</code> or <code class="language-plaintext highlighter-rouge">VAE</code> indexes (<code class="language-plaintext highlighter-rouge">AEV</code> for <code class="language-plaintext highlighter-rouge">[?a :g/to ?b]</code> as the attribute part is fixed and $A$ comes before $B$ in the variable order). A <code class="language-plaintext highlighter-rouge">Prefix</code> is just what we called a prefix above, a prefix of a potential result row. An <code class="language-plaintext highlighter-rouge">Extension</code> is what could potentially be added to a prefix to become a new longer prefix. So for the triangle case we would have for example a prefix $(a_i \space b_j)$ once we joined variables $A$ and $B$ and an extension $c_k$ when doing the join on $C$.</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">count</code> returns how many extensions there are for a given prefix.  As an example if the Datalog query contains the triple clause <code class="language-plaintext highlighter-rouge">[?person :person/name ?name]</code> and we are given the prefix <code class="language-plaintext highlighter-rouge">(1)</code> (just the entity id 1),  then the <code class="language-plaintext highlighter-rouge">count</code> would return the prefix count for <code class="language-plaintext highlighter-rouge">[:person/name 1]</code> in the <code class="language-plaintext highlighter-rouge">AEV</code> index. The attribute <code class="language-plaintext highlighter-rouge">:person/name</code> is fixed across the whole query so we won’t be part of the prefix, but just get fixed on the initialization of the <code class="language-plaintext highlighter-rouge">PrefixExtender</code></li>
  <li><code class="language-plaintext highlighter-rouge">propose</code> actually returns the extensions for a give prefix. For the <code class="language-plaintext highlighter-rouge">AEV</code> example above this will likely be just a name; a list of size one. If the attribute has multiplicative cardinality (as it the case for <code class="language-plaintext highlighter-rouge">g/to</code>) then the result will be all out-going edges from the already fixed node.</li>
  <li><code class="language-plaintext highlighter-rouge">intersect</code> intersects a given list of extensions with the extensions of this particular <code class="language-plaintext highlighter-rouge">PrefixExtender</code>.</li>
</ul>

<p>Let’s now describe the algorithm for a single variable level. For the join variable order $(A, B, C)$ and the triangle query, $R$ participates in levels 1 and 2 and $T$ participates in levels 1 and 3. In order for the algorithm to remain worst-case optimal (WCO) we need to start with the smallest extension set. Take the smallest extension set as a initial guess and then refine this initial guess based on the other <code class="language-plaintext highlighter-rouge">PrefixExtender</code>’s participating on that level.</p>

<p>In code:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">GenericSingleJoin</span><span class="p">(</span><span class="kd">val</span> <span class="py">extenders</span> <span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">PrefixExtender</span><span class="p">&gt;,</span> <span class="kd">val</span> <span class="py">prefixes</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Prefix</span><span class="p">&gt;)</span> <span class="p">:</span> <span class="nc">Join</span><span class="p">&lt;</span><span class="nc">Prefix</span><span class="p">&gt;</span> <span class="p">{</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">join</span><span class="p">():</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Prefix</span><span class="p">&gt;</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">results</span> <span class="p">=</span> <span class="n">mutableListOf</span><span class="p">&lt;</span><span class="nc">Prefix</span><span class="p">&gt;()</span>
        <span class="k">for</span> <span class="p">(</span><span class="n">prefix</span> <span class="k">in</span> <span class="n">prefixes</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// For every prefix find the extender that proposes the least extensions</span>
            <span class="kd">val</span> <span class="py">minIndex</span> <span class="p">=</span> <span class="n">extenders</span><span class="p">.</span><span class="n">indices</span><span class="p">.</span><span class="nf">minBy</span> <span class="p">{</span> <span class="n">extenders</span><span class="p">[</span><span class="n">it</span><span class="p">].</span><span class="nf">count</span><span class="p">(</span><span class="n">prefix</span><span class="p">)</span> <span class="p">}</span>
            <span class="c1">// Propose extensions from that extender</span>
            <span class="kd">var</span> <span class="py">extensions</span> <span class="p">=</span> <span class="n">extenders</span><span class="p">[</span><span class="n">minIndex</span><span class="p">].</span><span class="nf">propose</span><span class="p">(</span><span class="n">prefix</span><span class="p">)</span>
            <span class="c1">// Intersect with all other extenders</span>
            <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="k">in</span> <span class="n">extenders</span><span class="p">.</span><span class="n">indices</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">i</span> <span class="p">!=</span> <span class="n">minIndex</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">extensions</span> <span class="p">=</span> <span class="n">extenders</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="nf">intersect</span><span class="p">(</span><span class="n">prefix</span><span class="p">,</span> <span class="n">extensions</span><span class="p">)</span>
                <span class="p">}</span>
            <span class="p">}</span>
            <span class="n">results</span><span class="p">.</span><span class="nf">addAll</span><span class="p">(</span><span class="nf">applyExtensions</span><span class="p">(</span><span class="n">prefix</span><span class="p">,</span> <span class="n">extensions</span><span class="p">))</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">results</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">applyExtensions</code> is function that for a given prefix creates a new set of result prefixes, the prefix concatenated with each extension. The important part, the one that guarantees us worst-case optimality,  is that the intersection starts of with the minimal set of extensions. Subsequent intersections can then all be done in time proportional to this smallest set. The actual proof of this claim is part of the paper introducing GenericJoin<sup id="fnref:2:1" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">1</a></sup>.</p>

<p>To see why the smallest intersection choice matters, consider extending a prefix $(a_i, b_j)$ to find valid $c$ values in the triangle query. Relation $S$ proposes all nodes reachable from $b_j$, while $T$ proposes all nodes that have edges back to $a_i$. If $b_j$ is a high-degree node with many outgoing edges but only a few of those destinations connect back to $a_i$, starting with $S$’s large proposal wastes work. By always beginning with the smallest proposer, the total intersection work is bounded by $(\text{min count}) \times (\text{number of relations})$ rather than being dominated by the largest relation’s contribution.</p>

<p>One other important part for this algorithm to work efficiently is that the relations involved are indexed by the variables appearing in the prefix (to efficiently look up a proposal and do intersection with previous extensions). This is most likely not the case for your average SQL table, but it is the case for most variations of Entity-Attribute-Value triple indices.</p>

<p>Now putting the whole algorithm together, it mainly remains to call <code class="language-plaintext highlighter-rouge">GenericSingleJoin</code> at every level.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">GenericJoin</span><span class="p">(</span><span class="kd">val</span> <span class="py">extenders</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">PrefixExtender</span><span class="p">&gt;,</span> <span class="n">levels</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Join</span><span class="p">&lt;</span><span class="nc">ResultTuple</span><span class="p">&gt;</span> <span class="p">{</span>

    <span class="c1">// Precompute which extenders participate in which levels</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">extenderSets</span> <span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">List</span><span class="p">&lt;</span><span class="nc">PrefixExtender</span><span class="p">&gt;&gt;</span> <span class="p">=</span> <span class="nc">List</span><span class="p">(</span><span class="n">levels</span><span class="p">)</span> <span class="p">{</span> <span class="n">level</span> <span class="p">-&gt;</span>
        <span class="kd">val</span> <span class="py">participants</span> <span class="p">=</span> <span class="n">mutableListOf</span><span class="p">&lt;</span><span class="nc">PrefixExtender</span><span class="p">&gt;()</span>
        <span class="k">for</span> <span class="p">(</span><span class="n">extender</span> <span class="k">in</span> <span class="n">extenders</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">extender</span><span class="p">.</span><span class="nf">participatesInLevel</span><span class="p">(</span><span class="n">level</span><span class="p">))</span> <span class="p">{</span>
                <span class="n">participants</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">extender</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="n">participants</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">join</span><span class="p">():</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">ResultTuple</span><span class="p">&gt;</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="py">prefixes</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Prefix</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span><span class="nf">emptyList</span><span class="p">())</span>

        <span class="c1">// For every level, perform a single join with the extenders participating in that level</span>
        <span class="k">for</span> <span class="p">(</span><span class="n">extenderSet</span> <span class="k">in</span> <span class="n">extenderSets</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">singleJoin</span> <span class="p">=</span> <span class="nc">GenericSingleJoin</span><span class="p">(</span><span class="n">extenderSet</span><span class="p">,</span> <span class="n">prefixes</span><span class="p">)</span>
            <span class="n">prefixes</span> <span class="p">=</span> <span class="n">singleJoin</span><span class="p">.</span><span class="nf">join</span><span class="p">()</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">prefixes</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The extended prefixes at one level become the new prefixes at the next level and we start with an empty prefix.
At the last level the prefixes are just the result tuples.
And that’s it, in roughly ~50 LOC (modulo all the stuff I have left out) we have gotten an implementation of a WCOJ algorithm.
In the <a href="wcoj-datalog-and-genericjoin">next post</a> we will see how the <code class="language-plaintext highlighter-rouge">PrefixExtender</code> interface composes nicely for certain logical connectors of Datalog like <code class="language-plaintext highlighter-rouge">not</code>, <code class="language-plaintext highlighter-rouge">and</code> and <code class="language-plaintext highlighter-rouge">or</code>.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:2" role="doc-endnote">
      <p>The algorithm explained in this post is called GenericJoin and from the following paper <a href="https://arxiv.org/abs/1310.3314.">https://arxiv.org/abs/1310.3314.</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:2:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
    <li id="fn:1" role="doc-endnote">
      <p>You can find the code from this post at <a href="https://github.com/FiV0/hooray2">https://github.com/FiV0/hooray2</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Finn Völkel</name></author><summary type="html"><![CDATA[Explanation of a worst-case-optimal variable oriented join algorithm - GenericJoin 1 The algorithm explained in this post is called GenericJoin and from the following paper https://arxiv.org/abs/1310.3314. &#8617;]]></summary></entry><entry><title type="html">WCOJ - Graph-Join correspondence</title><link href="https://finnvolkel.com/wcoj-graph-join-correspondence" rel="alternate" type="text/html" title="WCOJ - Graph-Join correspondence" /><published>2025-12-09T00:00:00+00:00</published><updated>2025-12-09T00:00:00+00:00</updated><id>https://finnvolkel.com/wcoj-graph-join-correspondence</id><content type="html" xml:base="https://finnvolkel.com/wcoj-graph-join-correspondence"><![CDATA[<p><em>An introduction and motivation for Worst Case Optimal Joins</em></p>

<p>Consider the <a href="https://www.tpc.org/tpch/">TPC-H</a> query 5 (Local Supplier Volume) and just focus on the join part of the query. TPC-H is standardised benchmark with very common business queries on a synthetic dataset.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">SELECT</span>
    <span class="n">n_name</span><span class="p">,</span>
    <span class="k">SUM</span><span class="p">(</span><span class="n">l_extendedprice</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">l_discount</span><span class="p">))</span> <span class="k">AS</span> <span class="n">revenue</span>
<span class="k">FROM</span>
    <span class="n">customer</span>
    <span class="k">JOIN</span> <span class="n">orders</span> <span class="k">ON</span> <span class="n">c_custkey</span> <span class="o">=</span> <span class="n">o_custkey</span>
    <span class="k">JOIN</span> <span class="n">lineitem</span> <span class="k">ON</span> <span class="n">l_orderkey</span> <span class="o">=</span> <span class="n">o_orderkey</span>
    <span class="k">JOIN</span> <span class="n">supplier</span> <span class="k">ON</span> <span class="n">l_suppkey</span> <span class="o">=</span> <span class="n">s_suppkey</span> <span class="k">AND</span> <span class="n">c_nationkey</span> <span class="o">=</span> <span class="n">s_nationkey</span>
    <span class="k">JOIN</span> <span class="n">nation</span> <span class="k">ON</span> <span class="n">s_nationkey</span> <span class="o">=</span> <span class="n">n_nationkey</span>
    <span class="k">JOIN</span> <span class="n">region</span> <span class="k">ON</span> <span class="n">n_regionkey</span> <span class="o">=</span> <span class="n">r_regionkey</span>
<span class="k">WHERE</span>
    <span class="n">r_name</span> <span class="o">=</span> <span class="s1">'ASIA'</span>
    <span class="k">AND</span> <span class="n">o_orderdate</span> <span class="o">&gt;=</span> <span class="nb">DATE</span> <span class="s1">'1994-01-01'</span>
    <span class="k">AND</span> <span class="n">o_orderdate</span> <span class="o">&lt;</span> <span class="nb">DATE</span> <span class="s1">'1994-01-01'</span> <span class="o">+</span> <span class="n">INTERVAL</span> <span class="s1">'1'</span> <span class="nb">YEAR</span>
<span class="k">GROUP</span> <span class="k">BY</span>
    <span class="n">n_name</span>
<span class="k">ORDER</span> <span class="k">BY</span>
    <span class="n">revenue</span> <span class="k">DESC</span><span class="p">;</span> 
</code></pre></div></div>
<p>If we create a graph for this query where nodes represent join variables (in SQL these are just join conditions as the notion of a join variable does not really exist in SQL) and where edges represent relations (tables), we get the following diagram. Notice that this is a hypergraph as relations might participate in more than 2 joins.<img src="/assets/tpch_query5_hypergraph 1.png" alt="tpch_query5_hypergraph 1" />
There is a direct correspondence between graphs and joins. There are even whole database vendors who have made their data model <a href="https://en.wikipedia.org/wiki/Graph_database">graphs</a>. Albeit in most cases the underlying model is for standard graphs (binary edges). You can of course also go the other way around and model graphs in the relational model.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="k">g</span><span class="p">(</span><span class="n">f</span> <span class="nb">INT</span><span class="p">,</span> <span class="n">t</span> <span class="nb">INT</span><span class="p">);</span>
</code></pre></div></div>
<p>Finding all triangles in a graph can then be modelled as</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
    <span class="n">g1</span><span class="p">.</span><span class="n">f</span> <span class="k">AS</span> <span class="n">a</span><span class="p">,</span> <span class="n">g1</span><span class="p">.</span><span class="n">t</span> <span class="k">AS</span> <span class="n">b</span><span class="p">,</span> <span class="n">g2</span><span class="p">.</span><span class="n">t</span> <span class="k">AS</span> <span class="k">c</span>
<span class="k">FROM</span>
    <span class="k">g</span> <span class="k">AS</span> <span class="n">g1</span><span class="p">,</span> <span class="k">g</span> <span class="k">AS</span> <span class="n">g2</span><span class="p">,</span> <span class="k">g</span> <span class="k">AS</span> <span class="n">g3</span>
<span class="k">WHERE</span>
    <span class="n">g1</span><span class="p">.</span><span class="n">t</span> <span class="o">=</span> <span class="n">g2</span><span class="p">.</span><span class="n">f</span> <span class="k">AND</span> <span class="n">g2</span><span class="p">.</span><span class="n">t</span> <span class="o">=</span> <span class="n">g3</span><span class="p">.</span><span class="n">t</span> <span class="k">AND</span> <span class="n">g1</span><span class="p">.</span><span class="n">f</span> <span class="o">=</span> <span class="n">g3</span><span class="p">.</span><span class="n">f</span><span class="p">;</span>
</code></pre></div></div>
<p>The main difference between this query and former one is that we are doing self joins. The similarity is that we are looking for a certain kind of pattern in our data. A triangle in the latter case and a more involved pattern in the TPC-H case.</p>

<p>Writing the triangle in terms of three different relations you get</p>

\[Q(A,B,C) = R(A,B) \bowtie S(B,C) \bowtie T(C,A)\]

<p>and the corresponding hypergraph  (which is just a standard graph in this case)</p>

<p><img src="/assets/triangle_query_graph.png" alt="triangle_query_graph" /></p>

<p>I also want to have a look at the triangle query in <a href="https://v1-docs.xtdb.com/language-reference/1.24.3/datalog-queries/">EDN Datalog</a> . It looks in my opinion a lot cleaner and anticipates some of the explanations coming in later posts. The systems of the variant of Datalog I am interested in store data as EAV (Entity - Attribute - Value) triples (also sometimes called Subject - Predicate - Object in other contexts) where facts are stored as entities with an attribute name and a value. So for example</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mi">1</span><span class="w"> </span><span class="no">:name</span><span class="w"> </span><span class="s">"Ada Lovelace"</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>
<p>would mean an entity with id 1 (presumably a person), where the <code class="language-plaintext highlighter-rouge">name</code> is <code class="language-plaintext highlighter-rouge">"Ada Lovelace"</code>.
The triangle query in this model would look as follows. Entities are just nodes pointing at other nodes.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:find</span><span class="w"> </span><span class="p">[</span><span class="n">?a</span><span class="w"> </span><span class="n">?b</span><span class="w"> </span><span class="n">?c</span><span class="p">]</span><span class="w">
 </span><span class="no">:where</span><span class="w"> </span><span class="p">[[</span><span class="n">?a</span><span class="w"> </span><span class="no">:g/to</span><span class="w"> </span><span class="n">?b</span><span class="p">]</span><span class="w">
         </span><span class="p">[</span><span class="n">?a</span><span class="w"> </span><span class="no">:g/to</span><span class="w"> </span><span class="n">?c</span><span class="p">]</span><span class="w">
         </span><span class="p">[</span><span class="n">?b</span><span class="w"> </span><span class="no">:g/to</span><span class="w"> </span><span class="n">?c</span><span class="p">]]}</span><span class="w">
</span></code></pre></div></div>
<p>The symbols starting with <code class="language-plaintext highlighter-rouge">?</code> are variables and are implicitly joined (equi-join) with all other occurrences of the variable. The <code class="language-plaintext highlighter-rouge">:where</code> section of a query limits the combinations of possible results by pattern matching the clauses against the EAV index of the database. In this case 3 triple clauses that specify the edges. You can think of the <code class="language-plaintext highlighter-rouge">:find</code> part as the projection of the query. By default such databases have different permutations of the EAV index (most have at least<code class="language-plaintext highlighter-rouge">EAV</code>, <code class="language-plaintext highlighter-rouge">AEV</code> and <code class="language-plaintext highlighter-rouge">AVE</code> as indices). These indices will become important in later posts. In this model the edges of the graph have an implicit direction, but we can assume an edge is only present if <code class="language-plaintext highlighter-rouge">?from &lt; ?to</code> for all clauses matching <code class="language-plaintext highlighter-rouge">[?from :g/to ?to]</code>.</p>

<p>Once the relationship between graphs and joins is established, one can ask all kinds of questions on graphs and wonder what it means for joins.
Let’s take some common graph problems and see what that corresponds to in terms of joins (with the hypergraph definition from above):</p>
<ul>
  <li><a href="https://en.wikipedia.org/wiki/Vertex_cover">Vertex cover</a> - A set of variables (join conditions) that touch every relation of the query. Correspondingly a minimum vertex cover is the smallest set (in size) of variables touching every relation.</li>
  <li><a href="https://en.wikipedia.org/wiki/Independent_set_(graph_theory)">Independent set</a> - A set of variables that pairwise don’t share a relation.</li>
  <li><a href="https://en.wikipedia.org/wiki/Clique_(graph_theory)">Clique</a>- It’s just the dual of the independent set, so a set of variables that pairwise share a relation.</li>
  <li><a href="https://en.wikipedia.org/wiki/Edge_cover">Edge Cover</a> - A set of relations so that every variable (join) is covered by some relation. I want to formalize the edge cover a bit more as it will become important further down. Let $n$ be the number of relations. Let $H_Q$ be the hypergraph of a query as defined above. We let $E_Q$ be the (hyper) edges of the query graph. Informally we have $E_Q = \bigcup_{i \in [n]} R_i$ (relations are just edges). An edge cover of of $H_Q$ is then a subset of $E_Q$ so that every variable is covered. For the triangle query any two edges suffice. For the TPC-H example from above <code class="language-plaintext highlighter-rouge">orders</code>, <code class="language-plaintext highlighter-rouge">supplier</code> and <code class="language-plaintext highlighter-rouge">region</code> would be an edge cover.</li>
</ul>

<p>This list could go on and on. I think the main takeaway here is that results in graph theory might have immediate implications for joins and it’s likely good to switch back and forth between these two representations when working on a join problem.</p>

<p>In the following we assume that every row in a table / relation is unique. We are also only taking about equi-joins. This is not the case for SQL but will help us to keep things simple.</p>

<p>First let’s establish some bounds for simple binary joins <sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">1</a></sup> and then extend these to multijoins. I think the most obvious bound for a join is</p>

\[|R \bowtie S| \leq |R||S|\]

<p>You can have no more rows than the size of the tables multiplied. For this bound to be met every row would need to join with every other row. But as we are talking about equi-joins (or natural joins), a join can only reduce the size of a table so we also have the bound</p>

\[|A \bowtie B| \leq \min(|A|,|B|)\]

<p>If we extend this to multiple relations (let’s say $k$) that all participate in a join on variable $A$ , we obtain</p>

\[|R_1 \bowtie R_2 \dots \bowtie R_k| \leq \min_{i \in [k]} |R_i|\]

<p>An edge cover of the hypergraph of a query let’s us obtain a bound on the output size of the whole query. Let’s unpack this a bit. For every variable participating in the query there is a set of relations (edges) that cover this variable.  As the formula just above shows, any size of a participating relation is an upper bound for that particular join variable.</p>

<p>An edge cover of the query graph is a set of relations that cover every participating variable. This means the product of sizes of these relations is an upper bound on the output size of the whole query. Writing this down more formally. Let $x_i$ be 1 if $R_i \in E_Q$ and 0 otherwise. This let’s us then conveniently write $|R_i|^{x_i}$ to get the size of the relation depending on whether $R_i$ is participating in the edge cover or not. Putting all this notation into one formula we are getting for a given edge cover $E_Q$</p>

\[|R_1 \bowtie R_2 ... \bowtie R_n| \leq \prod_{i=1}^{n} |R_i|^{x_i} \tag{AGM}\]

<p>This is just $N^2$ for the triangle query as you only need to pick two edges to get an edge cover. For the TPC-H query above, an upper bound would be $|orders||supplier||region|$.</p>

<p>If you think just about the triangle query case you can also find this bound without any machinery. Once you have chosen a 2-path in a graph ($O(N^2)$),  the number of results can only go down as the closing edge of the triangle only restricts results.</p>

<p>This finally brings us to the graph-theoretic result that initially sparked the worst-case optimal join gold rush <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">2</a></sup>. I won’t go into the details of how this result is obtained as that is way beyond the scope of this post. The result shows that you can relax the integer constraints in $\text{(AGM)}$ of the edge cover from</p>

\[x_i \in \{0, 1\}\]

<p>to</p>

\[x_i \in [0, 1]\]

<p>and still obtain a valid bound on the output size of the query $Q$. Let $E_{Q(A)}$ be the set of hyperedges that participate in the join on variable $A$. The relaxed version would have the constraint that</p>

\[\sum_{i \in E_{Q(A)}} x_i \geq 1\]

<p>and also for all other join variables participating in the query. The relaxation from the binary decision of taking an edge or not into the edge cover to partially taking an edge is called a fractional edge cover. The bound you obtain via the relaxed edge cover is also often abbreviated as AGM bound.</p>

<p>We can get a simpler formula by making some assumptions. Let $Q$ be a query over relations $R_1, R_2, …, R_m$ with $|R_i| \approx N$  the output size of $Q$ can be bound by</p>

\[|Q| \leq N^{\rho^*} \leq N^\rho\]

<p>where $\rho$ is the size of the minimum edge cover and $\rho^*$ is the size of the minimum fractional edge cover. This simply plugging the assumption $|R_i| \approx N$ into $\text{(AGM)}$.</p>

<p>Coming back to the triangle query</p>

\[Q(A,B,C) = R(A,B) \bowtie S(B,C) \bowtie T(C,A)\]

<p>the bound then simplifies to</p>

\[|Q| \leq |R|^{1/2} \, |S|^{1/2} \, |T|^{1/2} \leq N ^{3/2}\]

<p>by assigning every edge of the triangle query hypergraph a weight of $1/2$. This assignment satisfies the relaxed constraints of the fractional edge cover. This actually means any graph can have at most $N^{3/2}$ triangles!!!</p>

<p>For joins it means that any binary join strategy for the triangle query will potentially produce $O(N^2)$ intermediate result rows but the AGM bound proves there can never be more than $O(N^{3/2})$ result rows. The whole idea of Worst Case Optimal Join (WCOJ henceforth) is to join the relations in such a manner that we don’t go over the worst-case bound (the specific bound depends of course on the query and the size of the involved relations).</p>

<p>A WCOJ algorithm guarantees that you are not doing worse then the most degenerate case. As an example, there are graphs that have $O(N^{3/2})$ triangles and in that case it assures that no larger than $O(N^{3/2})$ intermediate result sets are created. For any graph having $o(N^{3/2})$ triangles, a WCOJ algorithm does not guarantee you to run in $o(N^{3/2})$. This is the <em>worst</em> part of WCOJ. Also keep in mind that variable ordering still plays an important role in pruning early.</p>

<p>It’s unlikely that you search for arbitrary graph patterns in a relational database, but in theory you could and in graph databases it is also way more likely. The TPC-H example shows there are queries that contain cycles and where binary join strategies might exhibit degenerate behaviour. Also keep in mind that WCOJ is still in its infancy compared to binary joins. So even if a WCOJ might guarantee you certain properties, in practice it might be that binary joins fare very well as they have been optimized for decades. <sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup></p>

<p>In the <a href="/wcoj-generic-join">next post</a> we will look at concrete WCOJ algorithm.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:2" role="doc-endnote">
      <p>I picked some notation (and inspiration) from the following post. I highly recommend this blog post from Justin Jaffray which also provides a really good introduction to WCOJ <a href="https://justinjaffray.com/a-gentle-ish-introduction-to-worst-case-optimal-joins/">https://justinjaffray.com/a-gentle-ish-introduction-to-worst-case-optimal-joins/</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://arxiv.org/abs/1711.03860">https://arxiv.org/abs/1711.03860</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>See also <a href="https://arxiv.org/abs/2301.10841">https://arxiv.org/abs/2301.10841</a> for an approach trying to unify the two. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Finn Völkel</name></author><summary type="html"><![CDATA[An introduction and motivation for Worst Case Optimal Joins]]></summary></entry><entry><title type="html">Continuation-passing style in an expression engine</title><link href="https://finnvolkel.com/continuation-passing-style-in-an-expression-engine" rel="alternate" type="text/html" title="Continuation-passing style in an expression engine" /><published>2025-11-23T00:00:00+00:00</published><updated>2025-11-23T00:00:00+00:00</updated><id>https://finnvolkel.com/continuation-passing-style-in-an-expression-engine</id><content type="html" xml:base="https://finnvolkel.com/continuation-passing-style-in-an-expression-engine"><![CDATA[<p>Continuation Passing Style (CPS henceforth) is usually something from esoteric functional programming languages. I want to show you a little example of how it is used in the <a href="https://xtdb.com/">XTDB</a> Expression Engine. Before we start, let’s have a look at what CPS actually is. A continuation in a Lisp-flavoured language like Clojure is a function of the value of a subexpression to the value of the program (also an expression). So consider the expression<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">+</span><span class="w"> </span><span class="p">(</span><span class="nb">*</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nb">*</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="mi">4</span><span class="p">))</span><span class="w">
</span></code></pre></div></div>
<p>When have evaluated <code class="language-plaintext highlighter-rouge">1</code> the continuation of the whole expression is</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">v</span><span class="p">]</span><span class="w"> </span><span class="p">(</span><span class="nb">+</span><span class="w"> </span><span class="p">(</span><span class="nb">*</span><span class="w"> </span><span class="n">v</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nb">*</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="mi">4</span><span class="p">)))</span><span class="w">
</span></code></pre></div></div>
<p>So a continuation is a function itself (if the programming language supports functions). A general function transformed to CPS style, is a function that always takes an extra argument, the continuation. CPS style functions don’t return a value but call the passed in continuation with their result value. So to get an actual value out of an CPS style function, you need to call it with <code class="language-plaintext highlighter-rouge">identity</code> as continuation.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">;; Standard naive factorial implementation</span><span class="w">
</span><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">fac</span><span class="w"> </span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="w">
  </span><span class="p">(</span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nb">zero?</span><span class="w"> </span><span class="n">n</span><span class="p">)</span><span class="w">
    </span><span class="mi">1</span><span class="w">
    </span><span class="p">(</span><span class="nb">*</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="p">(</span><span class="nf">fac</span><span class="w"> </span><span class="p">(</span><span class="nb">dec</span><span class="w"> </span><span class="n">n</span><span class="p">)))))</span><span class="w">

</span><span class="p">(</span><span class="nf">fac</span><span class="w"> </span><span class="mi">5</span><span class="p">)</span><span class="w">
</span><span class="c1">;; =&gt; 120</span><span class="w">

</span><span class="c1">;; CPS style factorial implementation</span><span class="w">
</span><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">fac-cps</span><span class="w"> </span><span class="p">[</span><span class="n">n</span><span class="w"> </span><span class="n">cont</span><span class="p">]</span><span class="w">
  </span><span class="p">(</span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nb">zero?</span><span class="w"> </span><span class="n">n</span><span class="p">)</span><span class="w">
    </span><span class="p">(</span><span class="nf">cont</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w">
    </span><span class="p">(</span><span class="nf">fac-cps</span><span class="w"> </span><span class="p">(</span><span class="nb">dec</span><span class="w"> </span><span class="n">n</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">res</span><span class="p">]</span><span class="w"> </span><span class="p">(</span><span class="nf">cont</span><span class="w"> </span><span class="p">(</span><span class="nb">*</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="n">res</span><span class="p">))))))</span><span class="w">

</span><span class="p">(</span><span class="nf">fac-cps</span><span class="w"> </span><span class="mi">5</span><span class="w"> </span><span class="nb">identity</span><span class="p">)</span><span class="w">
</span><span class="c1">;; =&gt; 120</span><span class="w">
</span></code></pre></div></div>

<p>You might wonder why you ever want to program like this. It seems extremely cumbersome.</p>
<h3 id="a-little-language">A little language</h3>

<p>Let’s take a step back and focus on something quite different first to motivate our final example. Let’s build a little expression engine. The language for our example is quite limited but will suffice to convey the ideas.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">;; null literals</span><span class="w">
</span><span class="n">nil</span><span class="w">
</span><span class="c1">;; number literals </span><span class="w">
</span><span class="mi">1</span><span class="w"> 
</span><span class="c1">;; addition</span><span class="w">
</span><span class="p">(</span><span class="nb">+</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span><span class="w">
</span><span class="c1">;; variables (just symbols)</span><span class="w">
</span><span class="n">x</span><span class="w">
</span><span class="c1">;; conditionals / branching </span><span class="w">
</span><span class="o">'</span><span class="p">(</span><span class="k">if</span><span class="w"> </span><span class="n">condition</span><span class="w"> </span><span class="n">if-branch</span><span class="w"> </span><span class="n">else-branch</span><span class="p">)</span><span class="w">
</span><span class="c1">; local bindings</span><span class="w">
</span><span class="o">'</span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="nb">binding</span><span class="w"> </span><span class="n">expr</span><span class="p">]</span><span class="w"> 
   </span><span class="n">body</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>
<p>Every expression in this language either produces null or an integer. For semantics we follow <a href="https://en.wikipedia.org/wiki/Null_(SQL)#Comparisons_with_NULL_and_the_three-valued_logic_(3VL)">SQL’s</a> <a href="https://en.wikipedia.org/wiki/Three-valued_logic">three-valued logic</a> approach. Hence any addition involving a <code class="language-plaintext highlighter-rouge">nil</code> results in <code class="language-plaintext highlighter-rouge">nil</code> and as we don’t have <code class="language-plaintext highlighter-rouge">true</code> or <code class="language-plaintext highlighter-rouge">false</code> in the language anything other then <code class="language-plaintext highlighter-rouge">nil</code> is considered truthy.</p>

<h3 id="an-interpreter">An interpreter</h3>
<p>Let’s start with a simple interpreter. First lets define a protocol that can be invoked on any parsed expression<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nf">defprotocol</span><span class="w"> </span><span class="n">Expr</span><span class="w">
  </span><span class="p">(</span><span class="nf">invoke</span><span class="w"> </span><span class="p">[</span><span class="n">this</span><span class="w"> </span><span class="n">env</span><span class="p">]))</span><span class="w">
</span></code></pre></div></div>
<p>The protocol is going to take the parsed expression and the environment containing any instantiation of variables.</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nf">defrecord</span><span class="w"> </span><span class="n">NullExpr</span><span class="w"> </span><span class="p">[]</span><span class="w">
  </span><span class="n">Expr</span><span class="w">
  </span><span class="p">(</span><span class="nf">invoke</span><span class="w"> </span><span class="p">[</span><span class="n">_</span><span class="w"> </span><span class="n">_env</span><span class="p">]</span><span class="w"> </span><span class="n">nil</span><span class="p">))</span><span class="w">

</span><span class="p">(</span><span class="nf">defrecord</span><span class="w"> </span><span class="n">LongExpr</span><span class="w"> </span><span class="p">[</span><span class="n">lng</span><span class="p">]</span><span class="w">
  </span><span class="n">Expr</span><span class="w">
  </span><span class="p">(</span><span class="nf">invoke</span><span class="w"> </span><span class="p">[</span><span class="n">_</span><span class="w"> </span><span class="n">_env</span><span class="p">]</span><span class="w"> </span><span class="n">lng</span><span class="p">))</span><span class="w">

</span><span class="p">(</span><span class="nf">defrecord</span><span class="w"> </span><span class="n">VarExpr</span><span class="w"> </span><span class="p">[</span><span class="n">var</span><span class="p">]</span><span class="w">
  </span><span class="n">Expr</span><span class="w">
  </span><span class="p">(</span><span class="nf">invoke</span><span class="w"> </span><span class="p">[</span><span class="n">_</span><span class="w"> </span><span class="n">env</span><span class="p">]</span><span class="w">
    </span><span class="p">(</span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nb">contains?</span><span class="w"> </span><span class="n">env</span><span class="w"> </span><span class="n">var</span><span class="p">)</span><span class="w">
      </span><span class="p">(</span><span class="nb">get</span><span class="w"> </span><span class="n">env</span><span class="w"> </span><span class="n">var</span><span class="p">)</span><span class="w">
      </span><span class="p">(</span><span class="nf">throw</span><span class="w"> </span><span class="p">(</span><span class="nf">UnsupportedOperationException.</span><span class="p">)))))</span><span class="w">

</span><span class="p">(</span><span class="nf">defrecord</span><span class="w"> </span><span class="n">PlusExpr</span><span class="w"> </span><span class="p">[</span><span class="n">args</span><span class="p">]</span><span class="w">
  </span><span class="n">Expr</span><span class="w">
  </span><span class="p">(</span><span class="nf">invoke</span><span class="w"> </span><span class="p">[</span><span class="n">_</span><span class="w"> </span><span class="n">env</span><span class="p">]</span><span class="w">
    </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">args</span><span class="w"> </span><span class="p">(</span><span class="nb">map</span><span class="w"> </span><span class="o">#</span><span class="p">(</span><span class="nf">invoke</span><span class="w"> </span><span class="n">%</span><span class="w"> </span><span class="n">env</span><span class="p">)</span><span class="w"> </span><span class="n">args</span><span class="p">)]</span><span class="w">
      </span><span class="p">(</span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nb">some</span><span class="w"> </span><span class="nb">nil?</span><span class="w"> </span><span class="n">args</span><span class="p">)</span><span class="w">
        </span><span class="n">nil</span><span class="w">
        </span><span class="p">(</span><span class="nb">apply</span><span class="w"> </span><span class="nb">+</span><span class="w"> </span><span class="n">args</span><span class="p">)))))</span><span class="w">

</span><span class="p">(</span><span class="nf">defrecord</span><span class="w"> </span><span class="n">IfExpr</span><span class="w"> </span><span class="p">[</span><span class="k">cond</span><span class="w"> </span><span class="n">if-branch</span><span class="w"> </span><span class="n">else-branch</span><span class="p">]</span><span class="w">
  </span><span class="n">Expr</span><span class="w">
  </span><span class="p">(</span><span class="nf">invoke</span><span class="w"> </span><span class="p">[</span><span class="n">_</span><span class="w"> </span><span class="n">env</span><span class="p">]</span><span class="w">
    </span><span class="p">(</span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nf">invoke</span><span class="w"> </span><span class="k">cond</span><span class="w"> </span><span class="n">env</span><span class="p">)</span><span class="w">
      </span><span class="p">(</span><span class="nf">invoke</span><span class="w"> </span><span class="n">if-branch</span><span class="w"> </span><span class="n">env</span><span class="p">)</span><span class="w">
      </span><span class="p">(</span><span class="nf">invoke</span><span class="w"> </span><span class="n">else-branch</span><span class="w"> </span><span class="n">env</span><span class="p">))))</span><span class="w">

</span><span class="p">(</span><span class="nf">defrecord</span><span class="w"> </span><span class="n">LetExpr</span><span class="w"> </span><span class="p">[</span><span class="nb">binding</span><span class="w"> </span><span class="n">b-expr</span><span class="w"> </span><span class="n">body</span><span class="p">]</span><span class="w">
  </span><span class="n">Expr</span><span class="w">
  </span><span class="p">(</span><span class="nf">invoke</span><span class="w"> </span><span class="p">[</span><span class="n">_</span><span class="w"> </span><span class="n">env</span><span class="p">]</span><span class="w">
    </span><span class="p">(</span><span class="nf">invoke</span><span class="w"> </span><span class="n">body</span><span class="w"> </span><span class="p">(</span><span class="nb">assoc</span><span class="w"> </span><span class="n">env</span><span class="w"> </span><span class="nb">binding</span><span class="w"> </span><span class="p">(</span><span class="nf">invoke</span><span class="w"> </span><span class="n">b-expr</span><span class="w"> </span><span class="n">env</span><span class="p">)))))</span><span class="w">
</span></code></pre></div></div>
<p>We are missing the parsing step.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defmulti</span><span class="w"> </span><span class="n">parse-expr</span><span class="w"> </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">expr</span><span class="p">]</span><span class="w">
                       </span><span class="p">(</span><span class="k">cond</span><span class="w"> </span><span class="p">(</span><span class="nb">nil?</span><span class="w"> </span><span class="n">expr</span><span class="p">)</span><span class="w"> </span><span class="no">:nil</span><span class="w">
                             </span><span class="p">(</span><span class="nf">number?</span><span class="w"> </span><span class="n">expr</span><span class="p">)</span><span class="w"> </span><span class="no">:long</span><span class="w">
                             </span><span class="p">(</span><span class="nb">symbol?</span><span class="w"> </span><span class="n">expr</span><span class="p">)</span><span class="w"> </span><span class="no">:var</span><span class="w">
                             </span><span class="p">(</span><span class="nf">list?</span><span class="w"> </span><span class="n">expr</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nb">keyword</span><span class="w"> </span><span class="p">(</span><span class="nb">first</span><span class="w"> </span><span class="n">expr</span><span class="p">)))))</span><span class="w">

</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">parse-expr</span><span class="w"> </span><span class="no">:nil</span><span class="w"> </span><span class="p">[</span><span class="n">_</span><span class="p">]</span><span class="w"> </span><span class="p">(</span><span class="nf">-&gt;NullExpr</span><span class="p">))</span><span class="w">
</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">parse-expr</span><span class="w"> </span><span class="no">:long</span><span class="w"> </span><span class="p">[</span><span class="n">lng</span><span class="p">]</span><span class="w"> </span><span class="p">(</span><span class="nf">-&gt;LongExpr</span><span class="w"> </span><span class="n">lng</span><span class="p">))</span><span class="w">
</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">parse-expr</span><span class="w"> </span><span class="no">:var</span><span class="w"> </span><span class="p">[</span><span class="n">var</span><span class="p">]</span><span class="w"> </span><span class="p">(</span><span class="nf">-&gt;VarExpr</span><span class="w"> </span><span class="n">var</span><span class="p">))</span><span class="w">
</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">parse-expr</span><span class="w"> </span><span class="no">:+</span><span class="w"> </span><span class="p">[[</span><span class="n">_</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="n">args</span><span class="p">]]</span><span class="w">
  </span><span class="p">(</span><span class="nf">-&gt;PlusExpr</span><span class="w"> </span><span class="p">(</span><span class="nb">map</span><span class="w"> </span><span class="n">parse-expr</span><span class="w"> </span><span class="n">args</span><span class="p">)))</span><span class="w">
</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">parse-expr</span><span class="w"> </span><span class="no">:if</span><span class="w"> </span><span class="p">[[</span><span class="n">_</span><span class="w"> </span><span class="k">cond</span><span class="w"> </span><span class="n">if-branch</span><span class="w"> </span><span class="n">else-branch</span><span class="p">]]</span><span class="w">
  </span><span class="p">(</span><span class="nf">-&gt;IfExpr</span><span class="w"> </span><span class="p">(</span><span class="nf">parse-expr</span><span class="w"> </span><span class="k">cond</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nf">parse-expr</span><span class="w"> </span><span class="n">if-branch</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nf">parse-expr</span><span class="w"> </span><span class="n">else-branch</span><span class="p">)))</span><span class="w">
</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">parse-expr</span><span class="w"> </span><span class="no">:let</span><span class="w"> </span><span class="p">[[</span><span class="n">_</span><span class="w"> </span><span class="p">[</span><span class="nb">binding</span><span class="w"> </span><span class="n">b-expr</span><span class="p">]</span><span class="w"> </span><span class="n">body</span><span class="p">]]</span><span class="w">
  </span><span class="p">(</span><span class="nf">-&gt;LetExpr</span><span class="w"> </span><span class="nb">binding</span><span class="w"> </span><span class="p">(</span><span class="nf">parse-expr</span><span class="w"> </span><span class="n">b-expr</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nf">parse-expr</span><span class="w"> </span><span class="n">body</span><span class="p">)))</span><span class="w">
</span></code></pre></div></div>

<p>Putting it all together:</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">-&gt;</span><span class="w"> </span><span class="p">(</span><span class="nf">parse-expr</span><span class="w"> </span><span class="o">'</span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">x</span><span class="w"> </span><span class="p">(</span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nb">+</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="n">y</span><span class="p">)</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="mi">4</span><span class="p">)]</span><span class="w">
                     </span><span class="p">(</span><span class="nb">+</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="n">x</span><span class="p">)))</span><span class="w">
    </span><span class="p">(</span><span class="nf">invoke</span><span class="w"> </span><span class="p">{</span><span class="ss">'y</span><span class="w"> </span><span class="mi">1</span><span class="p">}))</span><span class="w">
</span></code></pre></div></div>
<p>If this approach is used as an expression engine for an SQL engine <code class="language-plaintext highlighter-rouge">y</code> won’t be a single scalar but rather a vector for which the expression gets called on every element. The interpreter approach is therefore rather slow.</p>
<h3 id="a-direct-compiler">A direct compiler</h3>
<p>Let’s first define some interfaces for accessing elements in our vectors.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nf">defprotocol</span><span class="w"> </span><span class="n">IVectorReader</span><span class="w">
  </span><span class="p">(</span><span class="o">^</span><span class="n">void</span><span class="w"> </span><span class="n">isNull</span><span class="w"> </span><span class="p">[</span><span class="n">this</span><span class="w"> </span><span class="n">idx</span><span class="p">])</span><span class="w">
  </span><span class="p">(</span><span class="o">^</span><span class="nb">long</span><span class="w"> </span><span class="n">getLong</span><span class="w"> </span><span class="p">[</span><span class="n">this</span><span class="w"> </span><span class="n">idx</span><span class="p">]))</span><span class="w">

</span><span class="c1">;; In this world we only consider append only vectors</span><span class="w">
</span><span class="p">(</span><span class="nf">defprotocol</span><span class="w"> </span><span class="n">IVectorWriter</span><span class="w">
  </span><span class="p">(</span><span class="o">^</span><span class="n">void</span><span class="w"> </span><span class="n">writeNull</span><span class="w"> </span><span class="p">[</span><span class="n">this</span><span class="p">])</span><span class="w">
  </span><span class="p">(</span><span class="o">^</span><span class="n">void</span><span class="w"> </span><span class="n">writeLong</span><span class="w"> </span><span class="p">[</span><span class="n">this</span><span class="w"> </span><span class="n">lng</span><span class="p">]))</span><span class="w">
</span></code></pre></div></div>
<p>Let’s generate standard Clojure expressions from an incoming expression in our tiny toy language.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defmulti</span><span class="w"> </span><span class="n">codegen-direct</span><span class="w"> </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">expr</span><span class="p">]</span><span class="w"> </span><span class="p">(</span><span class="k">cond</span><span class="w"> </span><span class="p">(</span><span class="nb">nil?</span><span class="w"> </span><span class="n">expr</span><span class="p">)</span><span class="w"> </span><span class="no">:nil</span><span class="w">
                                          </span><span class="p">(</span><span class="nf">number?</span><span class="w"> </span><span class="n">expr</span><span class="p">)</span><span class="w"> </span><span class="no">:long</span><span class="w">
                                          </span><span class="p">(</span><span class="nb">symbol?</span><span class="w"> </span><span class="n">expr</span><span class="p">)</span><span class="w"> </span><span class="no">:var</span><span class="w">
                                          </span><span class="p">(</span><span class="nf">list?</span><span class="w"> </span><span class="n">expr</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nb">keyword</span><span class="w"> </span><span class="p">(</span><span class="nb">first</span><span class="w"> </span><span class="n">expr</span><span class="p">)))))</span><span class="w">

</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">codegen-direct</span><span class="w"> </span><span class="no">:nil</span><span class="w"> </span><span class="p">[</span><span class="n">_</span><span class="p">]</span><span class="w"> </span><span class="n">nil</span><span class="p">)</span><span class="w">
</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">codegen-direct</span><span class="w"> </span><span class="no">:long</span><span class="w"> </span><span class="p">[</span><span class="n">lng</span><span class="p">]</span><span class="w"> </span><span class="n">lng</span><span class="p">)</span><span class="w">

</span><span class="c1">;; The index we are accessing in all vector involved </span><span class="w">
</span><span class="p">(</span><span class="k">def</span><span class="w"> </span><span class="n">idx-sym</span><span class="w"> </span><span class="p">(</span><span class="nb">gensym</span><span class="w"> </span><span class="ss">'idx</span><span class="p">))</span><span class="w">

</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">codegen-direct</span><span class="w"> </span><span class="no">:var</span><span class="w"> </span><span class="p">[</span><span class="n">var</span><span class="p">]</span><span class="w">
  </span><span class="o">`</span><span class="p">(</span><span class="nb">when-not</span><span class="w"> </span><span class="p">(</span><span class="nf">.isNull</span><span class="w"> </span><span class="o">~</span><span class="n">var</span><span class="w"> </span><span class="o">~</span><span class="n">idx-sym</span><span class="p">)</span><span class="w">
     </span><span class="p">(</span><span class="nf">.getLong</span><span class="w"> </span><span class="o">~</span><span class="n">var</span><span class="w"> </span><span class="o">~</span><span class="n">idx-sym</span><span class="p">)))</span><span class="w">

</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">codegen-direct</span><span class="w"> </span><span class="no">:+</span><span class="w"> </span><span class="p">[[</span><span class="n">_</span><span class="w"> </span><span class="n">x-expr</span><span class="w"> </span><span class="n">y-expr</span><span class="p">]]</span><span class="w">
  </span><span class="o">`</span><span class="p">(</span><span class="nb">if-let</span><span class="w"> </span><span class="p">[</span><span class="n">x-res</span><span class="o">#</span><span class="w"> </span><span class="o">~</span><span class="p">(</span><span class="nf">codegen-direct</span><span class="w"> </span><span class="n">x-expr</span><span class="p">)]</span><span class="w">
     </span><span class="p">(</span><span class="nb">if-let</span><span class="w"> </span><span class="p">[</span><span class="n">y-res</span><span class="o">#</span><span class="w"> </span><span class="o">~</span><span class="p">(</span><span class="nf">codegen-direct</span><span class="w"> </span><span class="n">y-expr</span><span class="p">)]</span><span class="w">
       </span><span class="p">(</span><span class="nf">Math/addExact</span><span class="w"> </span><span class="n">x-res</span><span class="o">#</span><span class="w"> </span><span class="n">y-res</span><span class="o">#</span><span class="p">)</span><span class="w">
       </span><span class="n">nil</span><span class="p">)</span><span class="w">
     </span><span class="n">nil</span><span class="p">))</span><span class="w">

</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">codegen-direct</span><span class="w"> </span><span class="no">:if</span><span class="w"> </span><span class="p">[[</span><span class="n">_</span><span class="w"> </span><span class="k">cond</span><span class="w"> </span><span class="n">if-branch</span><span class="w"> </span><span class="n">else-branch</span><span class="p">]]</span><span class="w">
  </span><span class="o">`</span><span class="p">(</span><span class="k">if</span><span class="w"> </span><span class="o">~</span><span class="p">(</span><span class="nf">codegen-direct</span><span class="w"> </span><span class="k">cond</span><span class="p">)</span><span class="w">
     </span><span class="o">~</span><span class="p">(</span><span class="nf">codegen-direct</span><span class="w"> </span><span class="n">if-branch</span><span class="p">)</span><span class="w">
     </span><span class="o">~</span><span class="p">(</span><span class="nf">codegen-direct</span><span class="w"> </span><span class="n">else-branch</span><span class="p">)))</span><span class="w">

</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">codegen-direct</span><span class="w"> </span><span class="no">:let</span><span class="w"> </span><span class="p">[[</span><span class="n">_</span><span class="w"> </span><span class="p">[</span><span class="nb">binding</span><span class="w"> </span><span class="n">b-expr</span><span class="p">]</span><span class="w"> </span><span class="n">body</span><span class="p">]]</span><span class="w">
  </span><span class="o">`</span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="o">~</span><span class="nb">binding</span><span class="w"> </span><span class="p">(</span><span class="nf">-&gt;value-box</span><span class="p">)]</span><span class="w">
     </span><span class="p">(</span><span class="nb">if-let</span><span class="w"> </span><span class="p">[</span><span class="n">b-expr-res</span><span class="o">#</span><span class="w"> </span><span class="o">~</span><span class="p">(</span><span class="nf">codegen-direct</span><span class="w"> </span><span class="n">b-expr</span><span class="p">)]</span><span class="w">
       </span><span class="p">(</span><span class="nf">.writeLong</span><span class="w"> </span><span class="o">~</span><span class="nb">binding</span><span class="w"> </span><span class="n">b-expr-res</span><span class="o">#</span><span class="p">))</span><span class="w">
     </span><span class="o">~</span><span class="p">(</span><span class="nf">codegen-direct</span><span class="w"> </span><span class="n">body</span><span class="p">)))</span><span class="w">
</span></code></pre></div></div>
<p>Finally we need to setup the vectors and the index we are currently accessing around the generated expression.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">compile-expr</span><span class="w"> </span><span class="p">[</span><span class="n">expr</span><span class="w"> </span><span class="n">col-names</span><span class="p">]</span><span class="w">
  </span><span class="p">(</span><span class="nb">-&gt;</span><span class="w"> </span><span class="o">`</span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="o">~</span><span class="p">(</span><span class="nf">vec</span><span class="w"> </span><span class="n">col-names</span><span class="p">)</span><span class="w">
         </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">res-vec</span><span class="o">#</span><span class="w"> </span><span class="p">(</span><span class="nf">-&gt;vec-wrt</span><span class="p">)]</span><span class="w">
           </span><span class="p">(</span><span class="nb">dotimes</span><span class="w"> </span><span class="p">[</span><span class="o">~</span><span class="n">idx-sym</span><span class="w"> </span><span class="p">(</span><span class="nb">count</span><span class="w"> </span><span class="o">~</span><span class="p">(</span><span class="nb">first</span><span class="w"> </span><span class="n">col-names</span><span class="p">))]</span><span class="w">
             </span><span class="p">(</span><span class="nb">if-let</span><span class="w"> </span><span class="p">[</span><span class="n">res</span><span class="o">#</span><span class="w"> </span><span class="o">~</span><span class="p">(</span><span class="nf">codegen-direct</span><span class="w"> </span><span class="n">expr</span><span class="p">)]</span><span class="w">
               </span><span class="p">(</span><span class="nf">.writeLong</span><span class="w"> </span><span class="n">res-vec</span><span class="o">#</span><span class="w"> </span><span class="n">res</span><span class="o">#</span><span class="p">)</span><span class="w">
               </span><span class="p">(</span><span class="nf">.writeNull</span><span class="w"> </span><span class="n">res-vec</span><span class="o">#</span><span class="p">)))</span><span class="w">
           </span><span class="n">res-vec</span><span class="o">#</span><span class="p">))</span><span class="w">
      </span><span class="o">#</span><span class="n">_</span><span class="p">(</span><span class="nb">doto</span><span class="w"> </span><span class="n">clojure.pprint/pprint</span><span class="p">)</span><span class="w"> </span><span class="c1">; for debugging</span><span class="w">
      </span><span class="nb">eval</span><span class="p">))</span><span class="w">
</span></code></pre></div></div>
<p>Note: <em><code class="language-plaintext highlighter-rouge">-&gt;value-box</code> and <code class="language-plaintext highlighter-rouge">-&gt;vec-wrt</code> return implementations of the reader and writer interfaces above. The details don’t matter so much for this post except that <code class="language-plaintext highlighter-rouge">-&gt;value-box</code> is used for intermediate results and is purely a wrapper around a single value.</em></p>

<p>The function takes an expression in the toy language and a set of vector readers and then applies the generated code for the expression for each vector index (This implicitly assumes all vectors are the same length). We can write something like <code class="language-plaintext highlighter-rouge">(if-let [res# ~(codegen-direct expr)] ...)</code> because we know the only two possible result types in our language are <code class="language-plaintext highlighter-rouge">nil</code> and <code class="language-plaintext highlighter-rouge">number</code>. What happens if the result contains more types? What if the handling in the code generation is dependent on the type of a subexpression?</p>
<h3 id="a-cps-style-compiler">A CPS-style compiler</h3>
<p>The continuation function of our <code class="language-plaintext highlighter-rouge">compile-expr</code> call won’t only take the resulting generated code, but rather the return type as first argument and the generated code as second.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defmulti</span><span class="w"> </span><span class="n">codegen-expr</span><span class="w"> </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">expr</span><span class="w"> </span><span class="n">_cont</span><span class="p">]</span><span class="w">
                         </span><span class="p">(</span><span class="k">cond</span><span class="w"> </span><span class="p">(</span><span class="nb">nil?</span><span class="w"> </span><span class="n">expr</span><span class="p">)</span><span class="w"> </span><span class="no">:nil</span><span class="w">
                               </span><span class="p">(</span><span class="nf">number?</span><span class="w"> </span><span class="n">expr</span><span class="p">)</span><span class="w"> </span><span class="no">:long</span><span class="w">
                               </span><span class="p">(</span><span class="nb">symbol?</span><span class="w"> </span><span class="n">expr</span><span class="p">)</span><span class="w"> </span><span class="no">:var</span><span class="w">
                               </span><span class="p">(</span><span class="nf">list?</span><span class="w"> </span><span class="n">expr</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nb">keyword</span><span class="w"> </span><span class="p">(</span><span class="nb">first</span><span class="w"> </span><span class="n">expr</span><span class="p">)))))</span><span class="w">

</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">codegen-expr</span><span class="w"> </span><span class="no">:nil</span><span class="w"> </span><span class="p">[</span><span class="n">_</span><span class="w"> </span><span class="n">cont</span><span class="p">]</span><span class="w"> </span><span class="p">(</span><span class="nf">cont</span><span class="w"> </span><span class="no">:nil</span><span class="w"> </span><span class="n">nil</span><span class="p">))</span><span class="w">
</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">codegen-expr</span><span class="w"> </span><span class="no">:long</span><span class="w"> </span><span class="p">[</span><span class="n">lng</span><span class="w"> </span><span class="n">cont</span><span class="p">]</span><span class="w"> </span><span class="p">(</span><span class="nf">cont</span><span class="w"> </span><span class="no">:long</span><span class="w"> </span><span class="n">lng</span><span class="p">))</span><span class="w">
</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">codegen-expr</span><span class="w"> </span><span class="no">:var</span><span class="w"> </span><span class="p">[</span><span class="n">var</span><span class="w"> </span><span class="n">cont</span><span class="p">]</span><span class="w">
  </span><span class="o">`</span><span class="p">(</span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nf">.isNull</span><span class="w"> </span><span class="o">~</span><span class="n">var</span><span class="w"> </span><span class="o">~</span><span class="n">idx-sym</span><span class="p">)</span><span class="w">
     </span><span class="o">~</span><span class="p">(</span><span class="nf">cont</span><span class="w"> </span><span class="no">:nil</span><span class="w"> </span><span class="n">nil</span><span class="p">)</span><span class="w">
     </span><span class="o">~</span><span class="p">(</span><span class="nf">cont</span><span class="w"> </span><span class="no">:long</span><span class="w"> </span><span class="o">`</span><span class="p">(</span><span class="nf">.getLong</span><span class="w"> </span><span class="o">~</span><span class="n">var</span><span class="w"> </span><span class="o">~</span><span class="n">idx-sym</span><span class="p">))))</span><span class="w">

</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">codegen-expr</span><span class="w"> </span><span class="no">:+</span><span class="w"> </span><span class="p">[[</span><span class="n">_</span><span class="w"> </span><span class="n">x-expr</span><span class="w"> </span><span class="n">y-expr</span><span class="p">]</span><span class="w"> </span><span class="n">cont</span><span class="p">]</span><span class="w">
  </span><span class="p">(</span><span class="nf">codegen-expr</span><span class="w"> </span><span class="n">x-expr</span><span class="w">
                </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="n">continue-x</span><span class="w"> </span><span class="p">[</span><span class="n">x-type</span><span class="w"> </span><span class="n">x-code</span><span class="p">]</span><span class="w">
                  </span><span class="p">(</span><span class="nf">case</span><span class="w"> </span><span class="n">x-type</span><span class="w">
                    </span><span class="no">:long</span><span class="w"> </span><span class="p">(</span><span class="nf">codegen-expr</span><span class="w"> </span><span class="n">y-expr</span><span class="w">
                                        </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="n">continue-y</span><span class="w"> </span><span class="p">[</span><span class="n">y-type</span><span class="w"> </span><span class="n">y-code</span><span class="p">]</span><span class="w">
                                          </span><span class="p">(</span><span class="nf">case</span><span class="w"> </span><span class="n">y-type</span><span class="w">
                                            </span><span class="no">:long</span><span class="w"> </span><span class="p">(</span><span class="nf">cont</span><span class="w"> </span><span class="no">:long</span><span class="w"> </span><span class="o">`</span><span class="p">(</span><span class="nf">Math/addExact</span><span class="w"> </span><span class="o">~</span><span class="n">x-code</span><span class="w"> </span><span class="o">~</span><span class="n">y-code</span><span class="p">))</span><span class="w">
                                            </span><span class="no">:nil</span><span class="w"> </span><span class="p">(</span><span class="nf">cont</span><span class="w"> </span><span class="no">:nil</span><span class="w"> </span><span class="n">nil</span><span class="p">))))</span><span class="w">
                    </span><span class="no">:nil</span><span class="w"> </span><span class="p">(</span><span class="nf">cont</span><span class="w"> </span><span class="no">:nil</span><span class="w"> </span><span class="n">nil</span><span class="p">)))))</span><span class="w">

</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">codegen-expr</span><span class="w"> </span><span class="no">:if</span><span class="w"> </span><span class="p">[[</span><span class="n">_</span><span class="w"> </span><span class="k">cond</span><span class="w"> </span><span class="n">if-branch</span><span class="w"> </span><span class="n">else-branch</span><span class="p">]</span><span class="w"> </span><span class="n">cont</span><span class="p">]</span><span class="w">
  </span><span class="o">`</span><span class="p">(</span><span class="k">if</span><span class="w"> </span><span class="o">~</span><span class="p">(</span><span class="nf">codegen-expr</span><span class="w"> </span><span class="k">cond</span><span class="w"> </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">_</span><span class="w"> </span><span class="n">x</span><span class="p">]</span><span class="w"> </span><span class="n">x</span><span class="p">))</span><span class="w">
     </span><span class="o">~</span><span class="p">(</span><span class="nf">codegen-expr</span><span class="w"> </span><span class="n">if-branch</span><span class="w"> </span><span class="n">cont</span><span class="p">)</span><span class="w">
     </span><span class="o">~</span><span class="p">(</span><span class="nf">codegen-expr</span><span class="w"> </span><span class="n">else-branch</span><span class="w"> </span><span class="n">cont</span><span class="p">)))</span><span class="w">

</span><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">codegen-expr</span><span class="w"> </span><span class="no">:let</span><span class="w"> </span><span class="p">[[</span><span class="n">_</span><span class="w"> </span><span class="p">[</span><span class="nb">binding</span><span class="w"> </span><span class="n">b-expr</span><span class="p">]</span><span class="w"> </span><span class="n">body</span><span class="p">]</span><span class="w"> </span><span class="n">cont</span><span class="p">]</span><span class="w">
  </span><span class="p">(</span><span class="nf">codegen-expr</span><span class="w"> </span><span class="n">b-expr</span><span class="w">
                </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">b-type</span><span class="w"> </span><span class="n">b-code</span><span class="p">]</span><span class="w">
                  </span><span class="o">`</span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="o">~</span><span class="nb">binding</span><span class="w"> </span><span class="p">(</span><span class="nf">-&gt;value-box</span><span class="p">)]</span><span class="w">
                     </span><span class="o">~</span><span class="p">(</span><span class="nf">case</span><span class="w"> </span><span class="n">b-type</span><span class="w">
                        </span><span class="no">:nil</span><span class="w">
                        </span><span class="o">`</span><span class="p">(</span><span class="nf">do</span><span class="w">
                           </span><span class="p">(</span><span class="nf">.writeNull</span><span class="w"> </span><span class="o">~</span><span class="nb">binding</span><span class="p">)</span><span class="w">
                           </span><span class="o">~</span><span class="p">(</span><span class="nf">codegen-expr</span><span class="w"> </span><span class="n">body</span><span class="w"> </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">body-type</span><span class="w"> </span><span class="n">body-code</span><span class="p">]</span><span class="w">
                                                 </span><span class="p">(</span><span class="nf">cont</span><span class="w"> </span><span class="n">body-type</span><span class="w"> </span><span class="n">body-code</span><span class="p">))))</span><span class="w">
                        </span><span class="no">:long</span><span class="w">
                        </span><span class="o">`</span><span class="p">(</span><span class="nf">do</span><span class="w">
                           </span><span class="p">(</span><span class="nf">.writeLong</span><span class="w"> </span><span class="o">~</span><span class="nb">binding</span><span class="w"> </span><span class="o">~</span><span class="n">b-code</span><span class="p">)</span><span class="w">
                           </span><span class="o">~</span><span class="p">(</span><span class="nf">codegen-expr</span><span class="w"> </span><span class="n">body</span><span class="w"> </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">body-type</span><span class="w"> </span><span class="n">body-code</span><span class="p">]</span><span class="w">
                                                 </span><span class="p">(</span><span class="nf">cont</span><span class="w"> </span><span class="n">body-type</span><span class="w"> </span><span class="n">body-code</span><span class="p">)))))))))</span><span class="w">
</span></code></pre></div></div>
<p>Correspondingly to our direct compiler example we define a <code class="language-plaintext highlighter-rouge">compiler-expr2</code>.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">compile-expr2</span><span class="w"> </span><span class="p">[</span><span class="n">expr</span><span class="w"> </span><span class="n">col-names</span><span class="p">]</span><span class="w">
  </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">res-vec-sym</span><span class="w"> </span><span class="p">(</span><span class="nb">gensym</span><span class="w"> </span><span class="ss">'res-vec</span><span class="p">)]</span><span class="w">
    </span><span class="p">(</span><span class="nb">-&gt;</span><span class="w"> </span><span class="o">`</span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="o">~</span><span class="p">(</span><span class="nf">vec</span><span class="w"> </span><span class="n">col-names</span><span class="p">)</span><span class="w">
           </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="o">~</span><span class="n">res-vec-sym</span><span class="w"> </span><span class="p">(</span><span class="nf">-&gt;vec-wrt</span><span class="p">)]</span><span class="w">
             </span><span class="p">(</span><span class="nb">dotimes</span><span class="w"> </span><span class="p">[</span><span class="o">~</span><span class="n">idx-sym</span><span class="w"> </span><span class="p">(</span><span class="nb">count</span><span class="w"> </span><span class="o">~</span><span class="p">(</span><span class="nb">first</span><span class="w"> </span><span class="n">col-names</span><span class="p">))]</span><span class="w">
               </span><span class="o">~</span><span class="p">(</span><span class="nf">codegen-expr</span><span class="w"> </span><span class="n">expr</span><span class="w">
                              </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">out-type</span><span class="w"> </span><span class="n">out-code</span><span class="p">]</span><span class="w">
                                </span><span class="p">(</span><span class="nf">case</span><span class="w"> </span><span class="n">out-type</span><span class="w">
                                  </span><span class="no">:nil</span><span class="w"> </span><span class="o">`</span><span class="p">(</span><span class="nf">.writeNull</span><span class="w"> </span><span class="o">~</span><span class="n">res-vec-sym</span><span class="p">)</span><span class="w">
                                  </span><span class="no">:long</span><span class="w"> </span><span class="o">`</span><span class="p">(</span><span class="nf">.writeLong</span><span class="w"> </span><span class="o">~</span><span class="n">res-vec-sym</span><span class="w"> </span><span class="o">~</span><span class="n">out-code</span><span class="p">)))))</span><span class="w">
             </span><span class="o">~</span><span class="n">res-vec-sym</span><span class="p">))</span><span class="w">
        </span><span class="o">#</span><span class="n">_</span><span class="p">(</span><span class="nb">doto</span><span class="w"> </span><span class="n">clojure.pprint/pprint</span><span class="p">)</span><span class="w"> </span><span class="c1">; for debugging</span><span class="w">
        </span><span class="nb">eval</span><span class="p">)))</span><span class="w">
</span></code></pre></div></div>
<p>CPS helps to decide how (based on the type) to use the returned value (the code). For example look at the continuation passed in to the outermost <code class="language-plaintext highlighter-rouge">codegen-expr</code> call on line 7.</p>
<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">out-type</span><span class="w"> </span><span class="n">out-code</span><span class="p">]</span><span class="w">
    </span><span class="p">(</span><span class="nf">case</span><span class="w"> </span><span class="n">out-type</span><span class="w">
       </span><span class="no">:nil</span><span class="w"> </span><span class="o">`</span><span class="p">(</span><span class="nf">.writeNull</span><span class="w"> </span><span class="o">~</span><span class="n">res-vec-sym</span><span class="p">)</span><span class="w">
       </span><span class="no">:long</span><span class="w"> </span><span class="o">`</span><span class="p">(</span><span class="nf">.writeLong</span><span class="w"> </span><span class="o">~</span><span class="n">res-vec-sym</span><span class="w"> </span><span class="o">~</span><span class="n">out-code</span><span class="p">)))</span><span class="w">
</span></code></pre></div></div>
<p>We can branch based on the type of the expression without ever evaluating the expression. This becomes more powerful as your type systems grows.</p>

<p>For example an operator like <code class="language-plaintext highlighter-rouge">+</code> doesn’t need to know anything about the actual operands. The passed in result-type can be be used to decide how <code class="language-plaintext highlighter-rouge">+</code> should behave based on the first operand’s type. This could be polymorphic type handling or natural short-circuiting opportunities. In the <code class="language-plaintext highlighter-rouge">+</code> CPY-style <code class="language-plaintext highlighter-rouge">codegen-expr</code> implementation above we immediately cut short in case the <code class="language-plaintext highlighter-rouge">x-type</code> is <code class="language-plaintext highlighter-rouge">nil</code>. The result doesn’t need to know anything about how it will be used. This also makes the CPS-style approach quite easily extensible.</p>

<p>As XTDB handles union types. A direct compiler would need to do instance checks as the returned expression might be of an arbitrary type. The CPS style approach let’s you do dynamic dispatch on the result type without complicated casting.</p>

<p>The CPS offers in my opinion better logical locality (hardware locality is a different beast). The <code class="language-plaintext highlighter-rouge">type</code> and<code class="language-plaintext highlighter-rouge">code</code> forces all the necessary logic for a decision to be available locally. In a direct approach you will likely need get all this information from some passed through context or a previous pass.</p>

<p>All that to say that there are also downsides to the CPS approach.
An initial direct compiler version might be easier to implement and debug. The inversion of control in CPS makes a bit harder to wrap your head around at first.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Example shamelessly stolen from <a href="https://xavierleroy.org/control-structures/book/main008.html">Control structures</a> . <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>If you want to run the whole REPL session yourself. You can find the code <a href="https://github.com/FiV0/continuation-passing-style/blob/8f77d14c466d045f08326da1aa6c571536ffa32e/src/continuous_passing_style.clj">here</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Finn Völkel</name></author><summary type="html"><![CDATA[Continuation Passing Style (CPS henceforth) is usually something from esoteric functional programming languages. I want to show you a little example of how it is used in the XTDB Expression Engine. Before we start, let’s have a look at what CPS actually is. A continuation in a Lisp-flavoured language like Clojure is a function of the value of a subexpression to the value of the program (also an expression). So consider the expression1 (+ (* 1 2) (* 3 4)) When have evaluated 1 the continuation of the whole expression is (fn [v] (+ (* v 2) (* 3 4))) So a continuation is a function itself (if the programming language supports functions). A general function transformed to CPS style, is a function that always takes an extra argument, the continuation. CPS style functions don’t return a value but call the passed in continuation with their result value. So to get an actual value out of an CPS style function, you need to call it with identity as continuation. ```clojure ;; Standard naive factorial implementation (defn fac [n] (if (zero? n) 1 (* n (fac (dec n))))) Example shamelessly stolen from Control structures . &#8617;]]></summary></entry><entry><title type="html">XTDB’s transactional model</title><link href="https://finnvolkel.com/xtdbs-transactional-model" rel="alternate" type="text/html" title="XTDB’s transactional model" /><published>2025-05-29T00:00:00+00:00</published><updated>2025-05-29T00:00:00+00:00</updated><id>https://finnvolkel.com/xtdbs-transactional-model</id><content type="html" xml:base="https://finnvolkel.com/xtdbs-transactional-model"><![CDATA[<p><a href="https://transactional.blog/about">Alex Miller</a> recently wrote a good blog post in which he details how to <a href="https://transactional.blog/blog/2025-decomposing-transactional-systems">decompose transactional systems</a> according to the following steps:</p>

<blockquote>
  <p>Every transactional system does four things:</p>

  <ul>
    <li>It <em>executes</em> transactions.</li>
    <li>It <em>orders</em> transactions.</li>
    <li>It <em>validates</em> transactions.</li>
    <li>It <em>persists</em> transactions.</li>
  </ul>
</blockquote>

<p>In what follows, we will attempt to do just do that for XTDB (henceforth XT).
Even though <a href="https://v1-docs.xtdb.com/">XT v1</a> and <a href="https://docs.xtdb.com/">XT v2</a> are quite different on user level, the architecture according to the above is roughly the same across the two versions.</p>

<blockquote>
  <p><em>Ordering</em> a transaction means assigning the transaction some notion of a time at which it occurred.</p>
</blockquote>

<blockquote>
  <p><em>Persisting</em> a transaction makes it durable, generally to disk.</p>
</blockquote>

<p>XT is actually very similar to Calvin system described in the original post.
XT submits the transactions to a global log which <em>orders</em> and <em>persists</em> them (think of a Kafka log).
The submit timestamp of the log is the <a href="https://docs.xtdb.com/quickstart/sql-overview.html#system-time-columns-automatic-time-versioning-of-rows-without-audit-tables">system time</a> of the records going into XT.
Persistence here means the transaction is persisted and not the results of the transaction.
We therefore acknowledge a transaction when it hits the log (commit), but it still needs to get indexed.
The disadvantages are the same as for Calvin, <em>in that long running transactions will stall any later committed transactions from executing</em>.
XT and Calvin don’t support interactive (session-style) transactions either.</p>

<blockquote>
  <p><em>Validating</em> a transaction means enforcing concurrency control, or more rarely, domain-specific semantics.</p>
</blockquote>

<p>Each XT node reads transactions from the log and <em>validates</em> that the transaction timestamp is strictly greater then the last submitted transaction.
We also allow specifying a system time when submitting a transaction for backfilling systems and this case reject transactions with out of order manually specified timestamps.</p>

<p>As every transaction is processed in order on every node there is no Multiversion Concurrency Control.
At some point in the future we will could add sharding (potentially via multiple Kafka topics).</p>

<blockquote>
  <p><em>Executing</em> a transaction means evaluating the body of the transaction to produce the intended reads and writes.</p>
</blockquote>

<p>After validation, each node <em>executes</em> and indexes the transaction into the live index (an in memory structure) and eventually into a block which gets persisted to disk.
If a node shuts down or fails before the results of a transaction are written to disk, the live index can be rebuild from the log.</p>

<p><img src="/assets/xtdb_transactional_model.excalidraw.png" alt="XTDB's transaction model" /></p>

<p>You might be wondering where the coordination occurs in XT’s scenario.
As every node is indexing every transaction in order, the burden of coordination lies with the log and is outsourced in this case (Kafka as of this writing).</p>]]></content><author><name>Finn Völkel</name></author><summary type="html"><![CDATA[Alex Miller recently wrote a good blog post in which he details how to decompose transactional systems according to the following steps:]]></summary></entry><entry><title type="html">Window functions in practice</title><link href="https://finnvolkel.com/window-functions-in-practice" rel="alternate" type="text/html" title="Window functions in practice" /><published>2025-03-22T00:00:00+00:00</published><updated>2025-03-22T00:00:00+00:00</updated><id>https://finnvolkel.com/window-functions-in-practice</id><content type="html" xml:base="https://finnvolkel.com/window-functions-in-practice"><![CDATA[<p><em>TLDR: Some practical information of how to implement window functions based on the paper: <a href="https://vldb.org/pvldb/vol8/p1058-leis.pdf">Efficient Processing of Window Functions in Analytical SQL Queries, Leis et al.</a></em></p>

<p>Window functions allow you to compute values over a set of rows while preserving the original row. Unlike the <code class="language-plaintext highlighter-rouge">GROUP BY</code> operation, which aggregates the grouped rows into a single result, window functions maintain each row. This makes window functions ideal for calculating running totals, ranks, and moving averages.</p>

<p>Let us illustrate window functions with an example over sales data:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
  <span class="n">sale_id</span><span class="p">,</span>
  <span class="n">region</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">sale_amount</span><span class="p">)</span> <span class="n">OVER</span>
      <span class="p">(</span><span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">region</span>
       <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">_valid_from</span>
       <span class="k">RANGE</span> <span class="k">BETWEEN</span> <span class="n">INTERVAL</span> <span class="s1">'1 DAY'</span> <span class="k">AND</span> <span class="k">CURRENT</span> <span class="k">ROW</span><span class="p">)</span>
       <span class="k">AS</span> <span class="n">one_day_rolling_sum</span>
<span class="k">FROM</span> <span class="n">sales</span><span class="p">;</span>
</code></pre></div></div>

<p>The query above computes the 1-day running total of sales per region.</p>

<p>In SQL a window function consists of three main parts:</p>
<ul>
  <li>A <code class="language-plaintext highlighter-rouge">PARTIION BY</code> clause  specifies how the rows should be grouped, similar to <code class="language-plaintext highlighter-rouge">GROUP BY</code>, but without collapsing the groups. In the example above, the rows are partitioned by <code class="language-plaintext highlighter-rouge">region</code>.</li>
  <li>An <code class="language-plaintext highlighter-rouge">ORDER BY</code> clause defines the order in which rows are processed. In the example, <code class="language-plaintext highlighter-rouge">_valid_from</code> is used to indicate the time of the sale.</li>
  <li>A frame clause further refines the set rows to be considered around the current row in the calculation:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ROWS BETWEEN</code> operates on the physical rows before and after the current row.</li>
      <li><code class="language-plaintext highlighter-rouge">RANGE BETWEEN</code> operates on logical rows based on the ordering column, such as time intervals or numeric ranges.</li>
    </ul>
  </li>
</ul>

<p>Window functions are executed after <code class="language-plaintext highlighter-rouge">GROUP BY</code>, <code class="language-plaintext highlighter-rouge">HAVING</code>, and other expressions in the <code class="language-plaintext highlighter-rouge">SELECT</code> clause, but before the <code class="language-plaintext highlighter-rouge">ORDER BY</code> clause of the entire query. Each component of a window function is optional. If the <code class="language-plaintext highlighter-rouge">PARTITION BY</code> clause is omitted, the entire result set is treated as a single partition. Without an <code class="language-plaintext highlighter-rouge">ORDER BY</code> clause, rows are processed in the order they are received. The default frame specification is <code class="language-plaintext highlighter-rouge">BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW</code>, which includes all rows from the start of the partition up to the current row.</p>

<p>Certain window function aggregates are unaffected by framing. Examples include <code class="language-plaintext highlighter-rouge">ROW_NUMBER()</code> (which provides the ordinal position within the partition), <code class="language-plaintext highlighter-rouge">RANK()</code> (which assigns ranks within the partition, handling ties by assigning the same rank and leaving gaps), and <code class="language-plaintext highlighter-rouge">DENSE_RANK()</code> (similar to <code class="language-plaintext highlighter-rouge">RANK()</code>, but without gaps for ties). On the other hand, aggregate functions like <code class="language-plaintext highlighter-rouge">SUM()</code> and analytical functions like <code class="language-plaintext highlighter-rouge">FIRST_VALUE()</code> do utilize the window frame to compute their results.</p>

<p>It’s possible to calculate the values produced by window functions without actually using window functions. This often involves self joins and correlated subqueries. For instance, the example query above can be rewritten as follows:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
    <span class="n">s1</span><span class="p">.</span><span class="n">sale_id</span><span class="p">,</span>
    <span class="n">s1</span><span class="p">.</span><span class="n">region</span><span class="p">,</span>
    <span class="p">(</span>
        <span class="k">SELECT</span> <span class="k">SUM</span><span class="p">(</span><span class="n">s2</span><span class="p">.</span><span class="n">sale_amount</span><span class="p">)</span>
        <span class="k">FROM</span> <span class="n">sales</span> <span class="n">s2</span>
        <span class="k">WHERE</span> <span class="n">s2</span><span class="p">.</span><span class="n">region</span> <span class="o">=</span> <span class="n">s1</span><span class="p">.</span><span class="n">region</span>
          <span class="k">AND</span> <span class="n">s2</span><span class="p">.</span><span class="n">_valid_from</span> <span class="k">BETWEEN</span> <span class="n">s1</span><span class="p">.</span><span class="n">_valid_from</span> <span class="o">-</span> <span class="n">INTERVAL</span> <span class="s1">'1 DAY'</span> <span class="k">AND</span> <span class="n">s1</span><span class="p">.</span><span class="n">_valid_from</span>
    <span class="p">)</span> <span class="k">AS</span> <span class="n">daily_cumulative_total</span>
<span class="k">FROM</span> <span class="n">sales</span> <span class="n">s1</span><span class="p">;</span>
</code></pre></div></div>
<p>However, this approach can result in an $O(N^2)$ runtime if the window frame is large, compared to $O(N \log N)$ or $O(N)$ when the implementation is done smartly when shifting the frame (more on that below).</p>
<h3 id="partitioning-and-sorting">Partitioning and sorting</h3>

<p>There are two primary approaches to partitioning and sorting data for window functions:</p>

<ul>
  <li>A hash-based approach first partitions the input data by the <code class="language-plaintext highlighter-rouge">PARTITION BY</code> attributes and then independently sorts each partition by the <code class="language-plaintext highlighter-rouge">ORDER BY</code> attributes.</li>
  <li>A purely sort-based approach combines the <code class="language-plaintext highlighter-rouge">PARTITION BY</code> and <code class="language-plaintext highlighter-rouge">ORDER BY</code> attributes and sorts the entire input.</li>
</ul>

<p>Theoretically, the hash-based approach is faster because sorting only needs to occur within each partition after the $O(N)$ hash partitioning phase, compared to an $O(N \log N)$ overall sort. As sorting likely needs to happen anyway (it’s possible to omit the <code class="language-plaintext highlighter-rouge">ORDER BY</code>), it can be simpler and more straightforward to implement.</p>

<p>Both approaches lend themselves well to parallelization. For the sort-based approach, a parallel sorting algorithm is sufficient. For parallel hash-partitioning each thread processes an independent chunk of the input data, creating hash groups for its chunk. Afterwards, the same hash groups from different chunks are combined into partitions. The sorting of these partitions can then be done independently for each chunk. If the partitions are large or heavily skewed, intra-partition parallelization via a parallel sort can be applied.</p>

<p><img src="/assets/window_functions_partitioning+sorting.excalidraw.png" alt="window_functions_partitioning+sorting" class="center-image" /></p>

<p>The above illustrates the different phases of the hash-based approach with two partitions and two threads.</p>
<h3 id="framing">Framing</h3>
<p>Once partitioning and sorting are complete, you can explore strategies of evaluating the window aggregate per row. The naive implementation would evaluate the frame for every row, resulting in an $O(N^2)$ runtime (where $N$ is the size of the partition) if the frame is large relative to the partition.</p>

<p>Consider a running sum over <code class="language-plaintext highlighter-rouge">ROWS BETWEEN 5 PRECEDING AND 5 FOLLOWING</code> . Many database systems solve this with a removable cumulative approach. When the frame shifts the new element gets added to the aggregate and the element that has dropped of the frame gets removed.</p>

<p><img src="/assets/cumulative_simple.excalidraw.png" alt="cumulative_simple" class="center-image" /></p>

<p>This works well for <code class="language-plaintext highlighter-rouge">SUM</code> and <code class="language-plaintext highlighter-rouge">AVG</code>, resulting in a linear runtime. For a window like <code class="language-plaintext highlighter-rouge">RANGE BETWEEN INTERVAL '2 HOURS' PRECEDING AND INTERVAL '2 HOURS' FOLLOWING</code> with a non uniform distribution of the partition with respect to the order by part (e.g. many sales happening in the morning), it might result in many rows getting added and dropped when the window shifts. Nevertheless,  each row is only added and removed once from the window frame aggregation.</p>

<p>For <code class="language-plaintext highlighter-rouge">MIN</code> and <code class="language-plaintext highlighter-rouge">MAX</code> aggregates it’s necessary to maintain an auxiliary ordered search tree or heap of the entries in the window. Elements are added and removed as from this data structure as needed, resulting in an additional log factor in the runtime complexity. The SQL specification mandates that the bounds for the frame are constants, meaning that the above approaches work reasonably well.</p>

<p>For the lols, let us discard that constraint and assume windows of the form
<code class="language-plaintext highlighter-rouge">SUM(b) OVER (ORDER BY a ROWS BETWEEN x PRECEDING AND y FOLLOWING</code> are allowed. When the frame shifts the aggregate can then change arbitrarily, meaning that the removable cumulative approach from the previous section will no longer help. To avoid reverting to a naive approach, segment trees come to the rescue. A segment tree stores the aggregate for a set of subranges and let’s you query the aggregate for any range in logarithmic time.</p>

<p><img src="/assets/sum_aggregate_segment.excalidraw.png" alt="sum_aggregate_segment" class="center-image" /></p>

<p>In the example above the aggregated sum for frames 1 and 2 can be calculated by summing the blue and red circled nodes respectively. When calculating an aggregate with the segment tree one can either traverse the tree bottom-up, starting with the frame bounds, or top-down if the tree nodes also contain their respective range attached.</p>

<p>The segment tree approach allows to calculate arbitrary changing frames in $O(N \log N)$.  It should be noted that the additional overhead of constructing and traversing the tree might be costly in practice compared to the simpler cumulative approach even if theoretically the segment tree is faster.</p>

<p>We have illustrated the technique with the <code class="language-plaintext highlighter-rouge">SUM</code> aggregate, but the approaches can be adapted to work for <code class="language-plaintext highlighter-rouge">AVG</code> and <code class="language-plaintext highlighter-rouge">STDDEV</code> by storing more information in the nodes. It is worth noting that a different strategy can be chosen based on the size or statistics of a partition. Once the segment tree has been build it’s straightforward to parallelize the work.</p>

<p>There are further optimizations that can be applied to window functions, particularly around the evaluation of multiple window functions. For instance, if one window function is over <code class="language-plaintext highlighter-rouge">PARTITION BY a, b</code> and another is over <code class="language-plaintext highlighter-rouge">PARTITION BY a, c</code>, you only need to calculate the partitioning for <code class="language-plaintext highlighter-rouge">a</code> once.</p>

<p>There a further optimisations one can adapt for window functions, mainly around the evaluation of multiple window functions. For instance, if one window function is over <code class="language-plaintext highlighter-rouge">PARTITION BY a, b</code> and another is over <code class="language-plaintext highlighter-rouge">PARTITION BY a, c</code>, you only need to calculate the partitioning for <code class="language-plaintext highlighter-rouge">a</code> once.</p>

<p>We recently added window functions to <a href="https://github.com/xtdb/xtdb">XTDB</a>, so maybe come by and say <a href="https://docs.xtdb.com/index.html">hi</a>.</p>

<h4 id="references">References</h4>
<p><a href="https://vldb.org/pvldb/vol8/p1058-leis.pdf">Efficient Processing of Window Functions in Analytical SQL Queries, Leis et al.</a>*</p>]]></content><author><name>Finn Völkel</name></author><summary type="html"><![CDATA[TLDR: Some practical information of how to implement window functions based on the paper: Efficient Processing of Window Functions in Analytical SQL Queries, Leis et al.]]></summary></entry></feed>