<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Joist | Blog</title><description/><link>https://joist-orm.io/</link><language>en</language><item><title>Dual Release Channels</title><link>https://joist-orm.io/blog/dual-release-channels/</link><guid isPermaLink="true">https://joist-orm.io/blog/dual-release-channels/</guid><description>Joist now supports dual release channels, allowing you to choose between a stable and a bleeding-edge version of the ORM.

</description><pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For awhile now, we’ve struggled with Joist release announcements—as in we just don’t do them. 😅&lt;/p&gt;
&lt;p&gt;The primary reason is that, unlike a VC-funded or even bootstrapped product-focused company, Joist’s development has historically been 100% driven by what we need at &lt;a href=&quot;https://homebound.com&quot;&gt;Homebound&lt;/a&gt;, delivering business value in our day-to-day feature work.&lt;/p&gt;
&lt;p&gt;So, when we need a new Joist feature to make our feature delivery easier/suck less, we typically need it “right now”—and so we build it and release it, without much concern for “should this warrant a major release?” or “Could we bundle a few of these major features up into a marketing-oriented release announcement?”&lt;/p&gt;
&lt;p&gt;We mostly just need to “get back to work”, so we merge our Joist PRs and move on.&lt;/p&gt;
&lt;p&gt;While doing so, we’ve admittedly been somewhat loose with semantic versioning. We do use &lt;a href=&quot;https://github.com/semantic-release/semantic-release&quot;&gt;semantic release&lt;/a&gt; to drive our releases in CI, but we’ve shied away from the &lt;code dir=&quot;auto&quot;&gt;!&lt;/code&gt; of denote “breaking changes”, and have just stayed &lt;code dir=&quot;auto&quot;&gt;1.x&lt;/code&gt; release world for…well, years at this point. We’ve just kept incrementing &lt;code dir=&quot;auto&quot;&gt;1.1&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;1.2&lt;/code&gt;, all the way up to &lt;code dir=&quot;auto&quot;&gt;1.470&lt;/code&gt;-something. 😬&lt;/p&gt;
&lt;p&gt;But with Joist 2.0 freshly released, we’re going to try a different approach: dual release channels.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stable&lt;/strong&gt;: This channel will follow semantic versioning (&lt;code dir=&quot;auto&quot;&gt;major.minor.patch&lt;/code&gt;) and be the default artifact in npmjs.
&lt;ul&gt;
&lt;li&gt;I.e. this channel what you’ll get with a &lt;code dir=&quot;auto&quot;&gt;yarn add joist-orm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;In the periodic releases, we’ll bundle up a month-or-two of work into an “official release”, with appropriate documentation and release notes.&lt;/li&gt;
&lt;li&gt;We’ll trigger these releases by periodically merging &lt;code dir=&quot;auto&quot;&gt;main&lt;/code&gt; into our &lt;code dir=&quot;auto&quot;&gt;release&lt;/code&gt; branch&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Next&lt;/strong&gt;: This channel will deploy on every merge to &lt;code dir=&quot;auto&quot;&gt;main&lt;/code&gt; and use the &lt;code dir=&quot;auto&quot;&gt;next&lt;/code&gt; tag in npmjs.
&lt;ul&gt;
&lt;li&gt;I.e. this channel what you’ll get with a &lt;code dir=&quot;auto&quot;&gt;yarn add joist-orm@next&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;next&lt;/code&gt; version numbers will be “the last stable release” plus “an incrementing number”, so if &lt;code dir=&quot;auto&quot;&gt;2.1.0&lt;/code&gt; was the last stable release, next releases would be &lt;code dir=&quot;auto&quot;&gt;2.1.0-next.1&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;2.1.0-next.2&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;Next releases &lt;strong&gt;will not follow semantic versioning&lt;/strong&gt;—they could be a mix of bug fixes, new non-breaking features, and breaking changes.&lt;/li&gt;
&lt;li&gt;This is basically our release process today, albeit with the new &lt;code dir=&quot;auto&quot;&gt;next&lt;/code&gt; tagging&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Our hope is that these two channels will let us both:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Keep releasing new features immediately to &lt;code dir=&quot;auto&quot;&gt;next&lt;/code&gt;, for pulling into our internal repositories
&lt;ul&gt;
&lt;li&gt;Without blocking our internal work on “the next major release in 3 months”, and&lt;/li&gt;
&lt;li&gt;Without worrying about artifically burning/increasing major release numbers,&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Periodically batch up changes into a &lt;code dir=&quot;auto&quot;&gt;stable&lt;/code&gt; release, and
&lt;ul&gt;
&lt;li&gt;Announce the new features/wins as an official release, with release notes, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We’re optimistic about the approach, and mostly regret that we didn’t start doing this sooner! 😅&lt;/p&gt;
&lt;p&gt;(Of course, even though this approach is “new to us”, the dual channel approach is definitely not new—React releases are done this way, and I assume for the same rationale: given Facebook has an internal mono repo, they likely want “the latest release” asap (similar to our internal builds), while using the periodic releases for the general public, who don’t necessarily want wake up every morning to a possibly breaking change.)&lt;/p&gt;</content:encoded></item><item><title>Bringing Back Unnest</title><link>https://joist-orm.io/blog/bringing-back-unnest/</link><guid isPermaLink="true">https://joist-orm.io/blog/bringing-back-unnest/</guid><pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The latest Joist release brings back leveraging the Postgres &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt; function (see &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1692&quot;&gt;the PR&lt;/a&gt;), for a nice 9% bump on our alpha latency-oriented &lt;a href=&quot;https://github.com/joist-orm/joist-benchmarks&quot;&gt;benchmarks&lt;/a&gt; (&lt;code dir=&quot;auto&quot;&gt;joist_v2&lt;/code&gt; is the merged PR):&lt;/p&gt;
&lt;img src=&quot;https://joist-orm.io/images/unnest.png&quot; alt=&quot;Unnest Performance&quot; width=&quot;500&quot;&gt;
&lt;div&gt;&lt;h2 id=&quot;what-is-unnest&quot;&gt;What is &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt;?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt; is a Postgres function that takes an array and returns a set of rows, one for each element in the array.&lt;/p&gt;
&lt;p&gt;The simplest example is converting one array and turning it into a set of rows:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- Pass in 1 array, get back 3 rows&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt; unnest(&lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[1, 2, 3]) &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; i;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;---&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rows&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;But you can also use multiple &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt; statements to get multiple columns on those rows:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;select&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;unnest(&lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[1, 2, 3]) &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; id,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;unnest(&lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[&apos;foo&apos;, &apos;bar&apos;, &apos;zaz&apos;]) &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; first_name;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id | first_name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;----+------------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; | foo&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt; | bar&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt; | zaz&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rows&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;why-unnest-is-useful&quot;&gt;Why &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt; is useful&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt; is great for bulk SQL statements, such as inserting 10 authors in one &lt;code dir=&quot;auto&quot;&gt;INSERT&lt;/code&gt;; without &lt;code dir=&quot;auto&quot;&gt;INSERT&lt;/code&gt; you might have 4 columns * 10 authors = 40 query parameters:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; authors (first_name, last_name, publisher_id, favorite_color)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; (?, ?, ?, ?). &lt;/span&gt;&lt;span&gt;-- #a1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(?, ?, ?, ?), &lt;/span&gt;&lt;span&gt;-- #a2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(?, ?, ?, ?), &lt;/span&gt;&lt;span&gt;-- #a3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(?, ?, ?, ?), &lt;/span&gt;&lt;span&gt;-- #a4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(?, ?, ?, ?), &lt;/span&gt;&lt;span&gt;-- #a5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(?, ?, ?, ?), &lt;/span&gt;&lt;span&gt;-- #a6&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(?, ?, ?, ?), &lt;/span&gt;&lt;span&gt;-- #a7&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(?, ?, ?, ?), &lt;/span&gt;&lt;span&gt;-- #a8&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(?, ?, ?, ?), &lt;/span&gt;&lt;span&gt;-- #a9&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(?, ?, ?, ?) &lt;/span&gt;&lt;span&gt;-- #a10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Where we have 10 &lt;code dir=&quot;auto&quot;&gt;first_name&lt;/code&gt; parameters, 10 &lt;code dir=&quot;auto&quot;&gt;last_name&lt;/code&gt; parameters, etc., but with &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt; we can instead send up “just 1 array per column”:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; authors (first_name, last_name, publisher_id, favorite_color)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; unnest(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;varchar&lt;/span&gt;&lt;span&gt;[], &lt;/span&gt;&lt;span&gt;-- first_names&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;varchar&lt;/span&gt;&lt;span&gt;[], &lt;/span&gt;&lt;span&gt;-- last_names&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;[],     &lt;/span&gt;&lt;span&gt;-- publisher_ids&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;varchar&lt;/span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;-- colors&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The benefits of fewer query parameters are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Smaller SQL statements going over the wire (one of our benchmarks saw 41kb of SQL without &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt;, and 350 bytes with &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;“Stable” SQL statement that don’t change for each “number of authors being updated”, so will have better prepared statement cache hit rates&lt;/li&gt;
&lt;li&gt;Observability tools like Datadog will also better group “stable” SQL statements with a fixed number of parameters&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is generally a well-known approach, i.e. TimeScale had a &lt;a href=&quot;https://www.tigerdata.com/blog/boosting-postgres-insert-performance&quot;&gt;blog post&lt;/a&gt; highlighting a 2x performance increase, albeit you have to get to fairly large update sizes to have this much impact.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;bringing-it-back&quot;&gt;Bringing it back?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Joist had previously used &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt; in our &lt;code dir=&quot;auto&quot;&gt;INSERT&lt;/code&gt;s and &lt;code dir=&quot;auto&quot;&gt;UPDATE&lt;/code&gt;s, but we’d removed it because it turns out &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt; is finicky with array columns—it “overflattens” and requires reactangular arrays.&lt;/p&gt;
&lt;p&gt;(I.e. array columns like &lt;code dir=&quot;auto&quot;&gt;varchar[]&lt;/code&gt; for storing multiple values &lt;code dir=&quot;auto&quot;&gt;favorite_colors = [&apos;red&apos;, &apos;blue&apos;]&lt;/code&gt; in a single column.)&lt;/p&gt;
&lt;p&gt;The 1st unfortunate thing with &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt; is that it “overflattens”, i.e. if we want to update two author’s &lt;code dir=&quot;auto&quot;&gt;favorite_colors&lt;/code&gt; columns using &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt;, we’d intuitively think “let’s just pass an array of arrays”, one array for each author:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- Pass two arrays, with two elements each&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- We expect to get back two rows of {red,blue} and {green,purple}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; unnest(&lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[array[&apos;red&apos;,&apos;blue&apos;],&lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[&apos;green&apos;,&apos;purple&apos;]]);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;unnest&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;--------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;red&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;blue&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;green&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;purple&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rows&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;…wait, we got 4 rows instead.&lt;/p&gt;
&lt;p&gt;Unfortunately this is just how &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt; works—when given 2-dimensional arrays (like a matrix), it creates a row per each value/cell in matrix.&lt;/p&gt;
&lt;p&gt;Another unfortunate wrinkle with &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt; is that our intuitive “array of arrays” creates fundamentally invalid arrays if the authors have a different number of favorite colors:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- Try to create 1 array of {red,blue} and 1 array of {purple}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; unnest(&lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[array[&apos;red&apos;,&apos;blue&apos;],&lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[&apos;purple&apos;]]);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- ERROR:  multidimensional arrays must have array expressions with matching dimensions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Our error is treating the &lt;code dir=&quot;auto&quot;&gt;varchar[][]&lt;/code&gt; as “an array of arrays”, when fundamentally Postgres treats it as “a single array, of two dimensions”, like mathematical n-dimensional arrays or matrices: they must be “rectangular” i.e. every row of our &lt;code dir=&quot;auto&quot;&gt;m x n&lt;/code&gt; matrix must be the same length (we’ve been trying to create “jagged” multidimensional arrays, which is not supported).&lt;/p&gt;
&lt;p&gt;One final wrinkle is, not only must all rows be the same length, but think about nullable columns—how could we set &lt;code dir=&quot;auto&quot;&gt;a1&lt;/code&gt; &lt;code dir=&quot;auto&quot;&gt;favorite_colors=&apos;red&apos;, &apos;blue&apos;]&lt;/code&gt; but then set &lt;code dir=&quot;auto&quot;&gt;a2&lt;/code&gt; &lt;code dir=&quot;auto&quot;&gt;favorite_colors=null&lt;/code&gt;? With &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt;s strict array limitations we cannot.&lt;/p&gt;
&lt;p&gt;The combination of these issues is why we’d previously removed &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt; usage, but now have introducing our own &lt;code dir=&quot;auto&quot;&gt;unnest_arrays&lt;/code&gt; custom function that solves each of these problems.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;unnest_arrays-custom-function&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;unnest_arrays&lt;/code&gt; Custom Function&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Our custom &lt;code dir=&quot;auto&quot;&gt;unnest_arrays&lt;/code&gt; function works around &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt;s limitations by coordinating with the Joist runtime to create 2-dimensional arrays that satisfy Postgres’s requirements, but still produce the desired values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When updating &lt;code dir=&quot;auto&quot;&gt;favorite_colors&lt;/code&gt; for multiple authors with different number of colors, we pad &lt;em&gt;trailing&lt;/em&gt; &lt;code dir=&quot;auto&quot;&gt;null&lt;/code&gt;s to the end of each author’s colors array, until the array is rectangular&lt;/li&gt;
&lt;li&gt;When updating &lt;code dir=&quot;auto&quot;&gt;favorite_colors&lt;/code&gt; to null, we also pad a single &lt;em&gt;leading&lt;/em&gt; &lt;code dir=&quot;auto&quot;&gt;null&lt;/code&gt; to indicate the desired nullness (and pad a “not-null marker” for other rows).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is simpler to see with an example, of updating three authors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Author 1 should update &lt;code dir=&quot;auto&quot;&gt;favorite_colors=red,green,blue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Author 2 should update &lt;code dir=&quot;auto&quot;&gt;favorite_colors=green&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Author 3 should update &lt;code dir=&quot;auto&quot;&gt;favorite_colors=null&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We are able to issue a SQL &lt;code dir=&quot;auto&quot;&gt;UPDATE&lt;/code&gt; like:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;WITH&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;unnest($&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;unnest_arrays($&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; favorite_colors,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;UPDATE&lt;/span&gt;&lt;span&gt; authors &lt;/span&gt;&lt;span&gt;SET&lt;/span&gt;&lt;span&gt; favorite_colors &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;favorite_colors&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;authors&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;And our &lt;code dir=&quot;auto&quot;&gt;favorite_colors&lt;/code&gt; array looks like:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- Created by the Joist runtime by reading the Author&apos;s favoriteColors&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- property and then adding padding as needed to a rectangular 2D array&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[&apos;&apos;, &apos;red&apos;, &apos;green&apos;, &apos;blue&apos;], &lt;/span&gt;&lt;span&gt;-- a:1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[&apos;&apos;, &apos;green&apos;, null, null], &lt;/span&gt;&lt;span&gt;-- a:2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[null, null, null, null]] &lt;/span&gt;&lt;span&gt;-- a:3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This array is passed our &lt;code dir=&quot;auto&quot;&gt;unnest_arrays&lt;/code&gt; custom function that knows about each of these conventions:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CREATE OR REPLACE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FUNCTION&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;unnest_arrays&lt;/span&gt;&lt;span&gt;(arr ANYARRAY, nullable &lt;/span&gt;&lt;span&gt;BOOLEAN&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; false, &lt;/span&gt;&lt;span&gt;OUT&lt;/span&gt;&lt;span&gt; a ANYARRAY)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;RETURNS&lt;/span&gt;&lt;span&gt; SETOF ANYARRAY&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;LANGUAGE&lt;/span&gt;&lt;span&gt; plpgsql IMMUTABLE STRICT &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$func$&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;BEGIN&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;FOREACH a SLICE &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IN&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ARRAY&lt;/span&gt;&lt;span&gt; arr &lt;/span&gt;&lt;span&gt;LOOP&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;-- When invoked for nullable columns, watch for the is-null/is-not-null marker&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;IF&lt;/span&gt;&lt;span&gt; nullable &lt;/span&gt;&lt;span&gt;THEN&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;-- If we should be null, drop all values and  return null&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;IF&lt;/span&gt;&lt;span&gt; a[1] &lt;/span&gt;&lt;span&gt;IS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NULL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;THEN&lt;/span&gt;&lt;span&gt; a :&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NULL&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;-- Otherwise drop the is-not-null  marker&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;ELSE&lt;/span&gt;&lt;span&gt; a :&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; a[2:array_length(a, 1)];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;END&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IF&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;END&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IF&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;-- Drop all remaining/trailing nulls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;a :&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; array_remove(a, &lt;/span&gt;&lt;span&gt;NULL&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;RETURN&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NEXT&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;END&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LOOP&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;END&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;And that’s it; we get out the other side our desired rows:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt; unnest_arrays(&lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[&apos;&apos;, &apos;red&apos;, &apos;green&apos;, &apos;blue&apos;], &lt;/span&gt;&lt;span&gt;-- a:1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[&apos;&apos;, &apos;green&apos;, null, null], &lt;/span&gt;&lt;span&gt;-- a:2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;[null, null, null, null]] &lt;/span&gt;&lt;span&gt;-- a:3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;, true);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;unnest_arrays&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;------------------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{red,green,blue}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{green}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rows&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;proscons&quot;&gt;Pros/Cons&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Our solution has a few pros/cons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pro: We’ve restored our ability to use &lt;code dir=&quot;auto&quot;&gt;unnest&lt;/code&gt; for all of our batched SELECTs, UPDATEs, and INSERTs 🎉&lt;/li&gt;
&lt;li&gt;Con: Joist users with array columns in their schemas will need to create the &lt;code dir=&quot;auto&quot;&gt;unnest_arrays&lt;/code&gt; function
&lt;ul&gt;
&lt;li&gt;This is a one-time migration so seems reasonable&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Con: With all the &lt;code dir=&quot;auto&quot;&gt;null&lt;/code&gt; padding tricks, we’re giving up the ability to have null values &lt;em&gt;within&lt;/em&gt; our array values
&lt;ul&gt;
&lt;li&gt;I.e. we cannot have &lt;code dir=&quot;auto&quot;&gt;favorite_colors=[red, null, blue]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;For our domain modeling purposes, this is a fine/acceptable tradeoff, b/c we’ve always modeled &lt;code dir=&quot;auto&quot;&gt;varchar[]&lt;/code&gt; columns as &lt;code dir=&quot;auto&quot;&gt;string[]&lt;/code&gt; and not &lt;code dir=&quot;auto&quot;&gt;Array&amp;#x3C;string | null&gt;&lt;/code&gt; — we actively don’t want &lt;code dir=&quot;auto&quot;&gt;null&lt;/code&gt;s in our &lt;code dir=&quot;auto&quot;&gt;varchar[]&lt;/code&gt; columns anyway&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So far these pros/cons are worth it 🚀; but, as always, we’ll continue adjusting our approach as we learn more from real-world use cases &amp;#x26; usage.&lt;/p&gt;</content:encoded></item><item><title>Using CTEs and Query Rewriting for Versioning</title><link>https://joist-orm.io/blog/query-rewriting-versioning/</link><guid isPermaLink="true">https://joist-orm.io/blog/query-rewriting-versioning/</guid><pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Joist is an ORM primarily developed for &lt;a href=&quot;https://homebound.com/&quot;&gt;Homebound&lt;/a&gt;’s GraphQL majestic monolith, and we recently shipped a long-awaited Joist feature, &lt;strong&gt;SQL query rewriting via an ORM plugin API&lt;/strong&gt;, to deliver a key component of our domain model: &lt;em&gt;aggregate level versioning&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;We’ll get into the nuanced details below, but “aggregate level versioning” is a fancy name for providing this &lt;em&gt;minor&lt;/em&gt; “it’s just a dropdown, right? 😰” feature of a version selector across several major subcomponents of our application:&lt;/p&gt;
&lt;img src=&quot;https://joist-orm.io/images/version-dropdown.png&quot; alt=&quot;Version dropdown&quot; width=&quot;500&quot;&gt;
&lt;p&gt;Where the user can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Time travel” back to a previous version of what they’re working on ⌛,&lt;/li&gt;
&lt;li&gt;Draft new changes (collaboratively with other users) that are not seen until they click “Publish” to make those changes active ✅&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And have the whole UI “just work” while they flip between the two.&lt;/p&gt;
&lt;p&gt;As a teaser, after some fits &amp;#x26; painful spurts, we achieved having our entire UI (and background processes) load historical “as of” values with just a few lines of setup/config per endpoint—and literally no other code changes. 🎉&lt;/p&gt;
&lt;p&gt;Read on to learn about our approach!&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;aggregate-what-now&quot;&gt;Aggregate What Now?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Besides just “versioning”, I called this “aggregate versioning”—what is that?&lt;/p&gt;
&lt;p&gt;It’s different from traditional database-wide, system time-based versioning, that auditing solutions like &lt;a href=&quot;https://pgxn.org/dist/cyanaudit/&quot;&gt;cyanaudit&lt;/a&gt; or temporal &lt;code dir=&quot;auto&quot;&gt;FOR SYSTEM_TIME AS OF&lt;/code&gt; queries provide (although we do use cyanaudit for our audit trail &amp;#x26; really like it!).&lt;/p&gt;
&lt;p&gt;Let’s back up and start with the term “aggregate”. An aggregate is a cluster of ~2-10+ “related entities” in your domain model (or “related tables” in your database schema). The cluster of course depends on your specific domain—examples might be “an author &amp;#x26; their books”, or “a customer &amp;#x26; their bank accounts &amp;#x26; profile information”.&lt;/p&gt;
&lt;p&gt;Typically there is “an aggregate parent” (called the “aggregate root”, since it sits at the root of the aggregate’s subgraph) that naturally “owns” the related children within the aggregate; i.e. the &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; aggregate root owns the &lt;code dir=&quot;auto&quot;&gt;Book&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;BookReview&lt;/code&gt; children; the &lt;code dir=&quot;auto&quot;&gt;Customer&lt;/code&gt; aggregate root owns the &lt;code dir=&quot;auto&quot;&gt;CustomerBankAccount&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;CustomerProfile&lt;/code&gt; entities.&lt;/p&gt;
&lt;aside aria-label=&quot;Tip&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Tip&lt;/p&gt;&lt;div&gt;&lt;p&gt;In your own domain model, if you see a naming pattern of &lt;code dir=&quot;auto&quot;&gt;Customer&lt;/code&gt;, and then lots of &lt;code dir=&quot;auto&quot;&gt;CustomerFoo&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;CustomerBar&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;CustomerZaz&lt;/code&gt; entities, all starting with a &lt;code dir=&quot;auto&quot;&gt;Customer...&lt;/code&gt; prefix, that is a hint that &lt;code dir=&quot;auto&quot;&gt;Customer&lt;/code&gt; is the aggregate root for that cluster (aggregate) of entities.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;Historically, Aggregate Roots are a pattern from Domain Driven Design, and mostly theoretically useful—they serve as a logical grouping, which is nice, but don’t always manifest as specific outcomes/details in the implementation (at least from what I’ve seen).&lt;/p&gt;
&lt;aside aria-label=&quot;Tip&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Tip&lt;/p&gt;&lt;div&gt;&lt;p&gt;Unless you are sharding! At which point the aggregate root’s primary key, i.e. &lt;code dir=&quot;auto&quot;&gt;Customer.id&lt;/code&gt;, makes a really great shard key for all the child entities within the aggregate root.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h3 id=&quot;why-version-an-aggregate&quot;&gt;Why Version An Aggregate?&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Normally Joist blog posts don’t focus on specific domains or verticals, but for the purposes of this post, it helps to know the problem we are solving.&lt;/p&gt;
&lt;p&gt;At Homebound, we’re a construction company that builds residential homes; our primary domain model supports the planning and execution of our procurement &amp;#x26; construction ops field teams.&lt;/p&gt;
&lt;p&gt;The domain model is large (currently 500+ tables), but two key components are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The architectural plans for a specific model of home (called a &lt;code dir=&quot;auto&quot;&gt;PlanPackage&lt;/code&gt;), and&lt;/li&gt;
&lt;li&gt;The design scheme for products that go into a group of similar plans (called a &lt;code dir=&quot;auto&quot;&gt;DesignPackage&lt;/code&gt;)—i.e. a “Modern Farmhouse” design package might be shared across ~4-5 separate, but similarly-sized/laid out, architectural plans&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both of these &lt;code dir=&quot;auto&quot;&gt;PlanPackage&lt;/code&gt;s and &lt;code dir=&quot;auto&quot;&gt;DesignPackage&lt;/code&gt;s are aggregate roots that encompass many child &lt;code dir=&quot;auto&quot;&gt;PlanPackage...&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;DesignPackage...&lt;/code&gt; entities within them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What rooms are in the plan? &lt;code dir=&quot;auto&quot;&gt;PlanPackageRoom&lt;/code&gt;s&lt;/li&gt;
&lt;li&gt;What materials &amp;#x26; labor are required (bricks, lumber, quantities)? &lt;code dir=&quot;auto&quot;&gt;PlanPackageScopeLine&lt;/code&gt;s&lt;/li&gt;
&lt;li&gt;What structural/floor plan options do we offer to customers? &lt;code dir=&quot;auto&quot;&gt;PlanPackageOption&lt;/code&gt;s
&lt;ul&gt;
&lt;li&gt;How do these options change the plan’s materials &amp;#x26; labor? A m2m between &lt;code dir=&quot;auto&quot;&gt;PlanPackageScopeLine&lt;/code&gt;s and &lt;code dir=&quot;auto&quot;&gt;PlanPackageOption&lt;/code&gt;s&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;What are the appliances in the kitchen? &lt;code dir=&quot;auto&quot;&gt;DesignPackageProduct&lt;/code&gt;s&lt;/li&gt;
&lt;li&gt;What spec levels, of Essential/Premium/etc, do we offer? &lt;code dir=&quot;auto&quot;&gt;DesignPackageOption&lt;/code&gt;s
&lt;ul&gt;
&lt;li&gt;How do these spec levels change the home’s products? A m2m between &lt;code dir=&quot;auto&quot;&gt;DesignPackageProduct&lt;/code&gt;s and &lt;code dir=&quot;auto&quot;&gt;DesignPackageOption&lt;/code&gt;s&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This web of interconnected data can all be modeled successfully (albeit somewhat tediously)—but we also want it versioned! 😬&lt;/p&gt;
&lt;p&gt;Change management is extremely important in construction—what was v10 of the &lt;code dir=&quot;auto&quot;&gt;PlanPackage&lt;/code&gt; last week? What is v15 of the &lt;code dir=&quot;auto&quot;&gt;PlanPackage&lt;/code&gt; this week? What changed in each version between v10 and v15? Are there new options available to homebuyers? What scope changed? Do we need new bids? Etc.&lt;/p&gt;
&lt;p&gt;And, pertintent to this blog post, we want each of the &lt;code dir=&quot;auto&quot;&gt;PlanPackage&lt;/code&gt;s and &lt;code dir=&quot;auto&quot;&gt;DesignPackage&lt;/code&gt;s respective “web of interconnected data” (the aggregate root &amp;#x26; its children) &lt;em&gt;versioned together as a group&lt;/em&gt; with &lt;em&gt;application-level&lt;/em&gt; versioning: multiple users collaborate on the data (making simultaneous edits across the &lt;code dir=&quot;auto&quot;&gt;PlanPackage&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;DesignPackage&lt;/code&gt;), co-creating the next draft, and then we “Publish” the next active version with a changelog of changes.&lt;/p&gt;
&lt;p&gt;After users draft &amp;#x26; publish the plans, and potentially start working on the next draft, our application needs to be able to load a complete “aggregate-wide snapshot” for “The Cabin Plan” &lt;code dir=&quot;auto&quot;&gt;PlanPackage v10&lt;/code&gt; (that was maybe published yesterday) and a complete “aggregate-wide snapshot” of “Modern Design Scheme” &lt;code dir=&quot;auto&quot;&gt;DesignPackage v8&lt;/code&gt; (that was maybe published a few days earlier), and glue them together in a complete home.&lt;/p&gt;
&lt;p&gt;Hopefully this gives you an idea of the problem—it’s basically like users are all collaborating on “the next version of the plan document”, but instead of it being a single Google Doc-type artifact that gets copy/pasted to create “v1” then “v2” then “v3”, the collaborative/versioned artifact is our deep, rich domain model (relational database tables) of construction data.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;schema-approach&quot;&gt;Schema Approach&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;After a few &lt;em&gt;cough&lt;/em&gt; “prototypes” &lt;em&gt;cough&lt;/em&gt; of database schemas for versioning in the earlier days of our app, we settled on a database schema we like: two tables per entity, a main “identity” table, and a “versions” table to store snapshots, i.e. something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt; table: stores the “current” author data (&lt;code dir=&quot;auto&quot;&gt;first_name&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;last_name&lt;/code&gt;, etc), with 1 row per author (we call this the “identity” table)&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;author_versions&lt;/code&gt; table: stores snapshots of the author over time (&lt;code dir=&quot;auto&quot;&gt;author_id&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;version_id&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;first_name&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;last_name&lt;/code&gt;), with 1 row &lt;em&gt;per version&lt;/em&gt; of each author (we call this the “version” table)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is an extremely common approach for versioning schemas, i.e. it’s effectively the same schema suggested by PostgreQL’s &lt;a href=&quot;https://wiki.postgresql.org/wiki/SQL2011Temporal#System_Time&quot;&gt;SQL2011Temporal&lt;/a&gt; docs, albeit technically they’re tracking system time, like an audit trail, and not our user-driven versioning.&lt;/p&gt;
&lt;p&gt;This leads to a few differences: the &lt;code dir=&quot;auto&quot;&gt;SQL2011Temporal&lt;/code&gt; history tables use a &lt;code dir=&quot;auto&quot;&gt;_system_time daterange&lt;/code&gt; column to present “when each row was applicable in time” (tracking system time), while we use two FKs that also form “an effective range”, but the range is not time-based, it’s version-based: “this row was applicable starting at &lt;code dir=&quot;auto&quot;&gt;first=v5&lt;/code&gt; until &lt;code dir=&quot;auto&quot;&gt;final=v10&lt;/code&gt;”.&lt;/p&gt;
&lt;p&gt;So if we had three versions of an &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; in our &lt;code dir=&quot;auto&quot;&gt;author_versions&lt;/code&gt; table, it would look like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;id=10 author_id=1 first_id=v10 final_id=v15 first_name=bob last_name=smith&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;From versions &lt;code dir=&quot;auto&quot;&gt;v10&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;v15&lt;/code&gt;, the &lt;code dir=&quot;auto&quot;&gt;author:1&lt;/code&gt; had a &lt;code dir=&quot;auto&quot;&gt;firstName=bob&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;id=11 author_id=1 first_id=v15 final_id=v20 first_name=fred last_name=smith&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;From versions &lt;code dir=&quot;auto&quot;&gt;v15&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;v20&lt;/code&gt;, the &lt;code dir=&quot;auto&quot;&gt;author:1&lt;/code&gt; had a &lt;code dir=&quot;auto&quot;&gt;firstName=fred&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;id=11 author_id=1 first_id=v20 final_id=null first_name=fred last_name=brown&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;From versions &lt;code dir=&quot;auto&quot;&gt;v20&lt;/code&gt; to now, the &lt;code dir=&quot;auto&quot;&gt;author:1&lt;/code&gt; had a &lt;code dir=&quot;auto&quot;&gt;lastName=brown&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We found this &lt;code dir=&quot;auto&quot;&gt;_versions&lt;/code&gt; table strikes a good balance of tradeoffs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It only stores changed rows—if &lt;code dir=&quot;auto&quot;&gt;v20&lt;/code&gt; of the aggregate root (i.e. our &lt;code dir=&quot;auto&quot;&gt;PlanPackage&lt;/code&gt;) only changed the &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt;s and two of its &lt;code dir=&quot;auto&quot;&gt;Book&lt;/code&gt;s, there will only be 1 &lt;code dir=&quot;auto&quot;&gt;author_versions&lt;/code&gt; and two &lt;code dir=&quot;auto&quot;&gt;book_versions&lt;/code&gt;, even if the Author has 100s of books (i.e. we avoid making full copies of the aggregate root on every version)&lt;/li&gt;
&lt;li&gt;When a row does change, we snapshot the entire row, instead of tracking only specific columns changes (storing the whole row takes more space, but makes historical/versioned queries much easier)
&lt;ul&gt;
&lt;li&gt;Technically we only include mutable columns in the &lt;code dir=&quot;auto&quot;&gt;_versions&lt;/code&gt; table, i.e. our entities often have immutable columns (like &lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt; flags or &lt;code dir=&quot;auto&quot;&gt;parent&lt;/code&gt; references) and we don’t bother copying these into the &lt;code dir=&quot;auto&quot;&gt;_versions&lt;/code&gt; tables.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;We only store 1 version row “per version”—i.e. if, while making &lt;code dir=&quot;auto&quot;&gt;PlanPackage&lt;/code&gt; &lt;code dir=&quot;auto&quot;&gt;v20&lt;/code&gt;, an &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt;s first name changes multiple times, we only keep the latest value in the draft &lt;code dir=&quot;auto&quot;&gt;author_versions&lt;/code&gt;s row.
&lt;ul&gt;
&lt;li&gt;This is different from auditing systems like SQL2011Temporal, where &lt;code dir=&quot;auto&quot;&gt;_history&lt;/code&gt; rows are immutable, and every change must create a new row—we did not need/want this level of granularity for our application-level versioning.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With this approach, we can reconstruct historical versions by finding the singular “effective version” for each entity, with queries like:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; author_versions av&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt; authors a &lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;where&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; ($&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;=&lt;/span&gt;&lt;span&gt; $&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;final_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;is&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;final_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; $&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;$1&lt;/code&gt; is whatever authors we’re looking for (here a simple “id matches”, but it could be an arbitrarily complex &lt;code dir=&quot;auto&quot;&gt;WHERE&lt;/code&gt; condition)&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;$2&lt;/code&gt; is the version of the aggregate root we’re “as of”, i.e. v10&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;$3&lt;/code&gt; is also the same version “as of”, i.e. v10&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The condition of &lt;code dir=&quot;auto&quot;&gt;first_id &amp;#x3C;= v10 &amp;#x3C; final_id&lt;/code&gt; finds the singular &lt;code dir=&quot;auto&quot;&gt;author_versions&lt;/code&gt; row that is “effective” or “active” in v10, even if the version itself was created in v5 (and either never replaced, or not replaced until “some version after v10” like v15).&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;but-what-is-current&quot;&gt;…but what is “Current”?&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;author_versions&lt;/code&gt; schema is hopefully fairly obvious/intuitive, but I left out a wrinkle: what data is stored in the &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt; table itself?&lt;/p&gt;
&lt;p&gt;Obviously it should be “the data for the author”…but which version of the author? The latest published data? Or latest/WIP draft data?&lt;/p&gt;
&lt;p&gt;Auditing solutions always put “latest draft” in &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt;, but that’s because the application itself is &lt;em&gt;always&lt;/em&gt; reading/writing data from &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt;, and often doesn’t even know that the auditing &lt;code dir=&quot;auto&quot;&gt;author_versions&lt;/code&gt; tables exist—but in our app, we need workflows &amp;#x26; UIs to regularly read “the published data” &amp;#x26; &lt;em&gt;ignore the WIP draft changes&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;So we considered the two options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt; table stores the &lt;em&gt;latest draft&lt;/em&gt; version (as in SQL2011Temporal), or&lt;/li&gt;
&lt;li&gt;The &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt; table stores the &lt;em&gt;latest published&lt;/em&gt; version,&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We sussed out pros/cons in a design doc:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Option 1. Store “latest draft” data in &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Pro: Writes with business logic “just read the Author and other entities” for validation&lt;/li&gt;
&lt;li&gt;Con: All readers, &lt;em&gt;even those that just want the published data&lt;/em&gt;, must use the &lt;code dir=&quot;auto&quot;&gt;_versions&lt;/code&gt; table to “reconstruct the published/active author”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Option 2. Store the “latest published” data in &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Pro: The safest b/c readers that “just read from &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt;” will not accidentally read draft/unpublished data (a big concern for us)&lt;/li&gt;
&lt;li&gt;Pro: Any reader that wants to “read latest” doesn’t have to know about versioning at all, and can just read the &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt; table as-is (also a big win)&lt;/li&gt;
&lt;li&gt;Con: Writes that have business logic must first “reconstruct the draft author” (and its draft books and its draft book reviews if apply cross-entity business rules/invariants) from the &lt;code dir=&quot;auto&quot;&gt;_versions&lt;/code&gt; tables&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;aside aria-label=&quot;Tip&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Tip&lt;/p&gt;&lt;div&gt;&lt;p&gt;By “reconstruct the [published or draft] author”, what we mean is that instead of “boring CRUD code” that just &lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt;s from the &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;books&lt;/code&gt;, etc. tables, version-aware code must:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;a) actually read from the &lt;code dir=&quot;auto&quot;&gt;author_verisons&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;book_versions&lt;/code&gt; tables,&lt;/li&gt;
&lt;li&gt;b) know “which version” it should use to find those authors/books
&lt;ul&gt;
&lt;li&gt;…and we might be looking for “PlanPackage v10” but “DesignPackage v8” so track &lt;em&gt;multiple, contextual versions&lt;/em&gt; within a single request, not just a single “as of” timestamp&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;c) do version-aware graph traversal, i.e. “boring walk the graph” code like &lt;code dir=&quot;auto&quot;&gt;for (const book in author.books)&lt;/code&gt; that is ubiquitous in our codebase needs the &lt;code dir=&quot;auto&quot;&gt;author.books&lt;/code&gt; relation to use the version-aware &lt;code dir=&quot;auto&quot;&gt;book_versions.author_id&lt;/code&gt; instead of the &lt;code dir=&quot;auto&quot;&gt;books.author_id&lt;/code&gt;, b/c the &lt;code dir=&quot;auto&quot;&gt;Book&lt;/code&gt; might have changed its &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; over time.
&lt;ul&gt;
&lt;li&gt;So nearly &lt;em&gt;every graph navigation&lt;/em&gt; needs checked for “is this relation actually versioned?”, and if so, opt-into the more complex, version-aware codepath.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;This “reconstruction” problem seemed very intricate/complicated, and we did not want to update our legacy callers (which were &lt;strong&gt;mostly reads&lt;/strong&gt;) to “do all this crazy version resolution”, so after the usual design doc review, group comments, etc., we decide to &lt;strong&gt;store latest published in &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The key rationale being:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We really did not want to instrument our legacy readers to know about &lt;code dir=&quot;auto&quot;&gt;_versions&lt;/code&gt; tables to avoid seeing draft data (we were worried about draft data leaking into existing UIs/reporting)&lt;/li&gt;
&lt;li&gt;We thought “teaching the writes to ‘reconstruct’ the draft subgraph” when applying validation rules would be the lesser evil.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(You can tell I’m foreshadowing this was maybe not the best choice.)&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;initial-approach-not-so-great&quot;&gt;Initial Approach: Not so great&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;With our versioning scheme in place, we started a large project for our first versioning-based feature: allowing home buyers to deeply personalize the products (sinks, cabinet hardware, wall paint colors, etc) in the home their buying (i.e. in v10 of Plan 1, we offered sinks 1/2/3, but in v11 of Plan 1, we now offer sinks 3/4/5).&lt;/p&gt;
&lt;p&gt;And it was a disaster.&lt;/p&gt;
&lt;p&gt;This new feature set was legitimately complicated, but “modeling complicated domain models” is &lt;em&gt;supposed to be Joist’s sweet spot&lt;/em&gt;—what was happening?&lt;/p&gt;
&lt;p&gt;We kept having bugs, regressions, and accidental complexity—that all boiled down to the write path (mutations) having to constantly “reconstruct the drafts” that it was reading &amp;#x26; writing.&lt;/p&gt;
&lt;p&gt;Normally in Joist, a &lt;code dir=&quot;auto&quot;&gt;saveAuthor&lt;/code&gt; mutation just “loads the author, sets some fields, and saves”. Easy!&lt;/p&gt;
&lt;p&gt;But with this versioning scheme:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the &lt;code dir=&quot;auto&quot;&gt;saveAuthor&lt;/code&gt; mutation has to first “reconstruct the latest-draft &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; from the &lt;code dir=&quot;auto&quot;&gt;author_versions&lt;/code&gt; (if it exists)&lt;/li&gt;
&lt;li&gt;any writes cannot be “just update the &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; row” (that would immediately publish the change), they need to be staged into the draft &lt;code dir=&quot;auto&quot;&gt;AuthorVersion&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;any Joist hooks or validation rules &lt;strong&gt;also&lt;/strong&gt; need to do the same thing:
&lt;ul&gt;
&lt;li&gt;validation rules have to “reconstruct the draft” view of the author + books + book reviews subgraph of data they’re validating,&lt;/li&gt;
&lt;li&gt;hooks that want to make reactive changes must also “stage the change” into a draft &lt;code dir=&quot;auto&quot;&gt;BookVersion&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;BookReviewVersion&lt;/code&gt; instead of “just setting some &lt;code dir=&quot;auto&quot;&gt;Book&lt;/code&gt; fields”.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We had helper methods for most of these incantations—but it was still terrible.&lt;/p&gt;
&lt;p&gt;After a few months of this, we were commiserating about “why does this suck so much?” and finally realized—well, duh, we were wrong:&lt;/p&gt;
&lt;p&gt;We had chosen to “optimize reads” (they could “just read from &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt;” table).&lt;/p&gt;
&lt;p&gt;But, in doing so, we threw writes under the bus—they needed to “read &amp;#x26; write from drafts”—and it was &lt;strong&gt;actually our write paths that are the most complicated part of our application&lt;/strong&gt;—validation rules, side effects, business process, all happen on the write path.&lt;/p&gt;
&lt;p&gt;We needed the write path to be easy.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;new-approach-write-to-identities&quot;&gt;New Approach: Write to Identities&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;We were fairly ecstatic about reversing direction, and storing drafts/writes directly in &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;books&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;This would drastically simplify all of our write paths (GraphQL mutations) back to “boring CRUD” code:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Look, no versioning code!&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;author&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;em&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;load&lt;/span&gt;&lt;span&gt;(Author&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;a:1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (author&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;shouldBeAllowedToUpdateFoo&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;author&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;foo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; em&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;flush&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;…but what about those reads? Wouldn’t moving the super-complicated “reconstruct the draft” code out of the writes (yay!), over into “reconstruct the published” reads (oh wait), be just as bad, or worse (which was the rationale for our original decision)?&lt;/p&gt;
&lt;p&gt;We wanted to avoid making the same mistake twice, and just “hot potato-ing” the write path disaster over to the read path.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;making-reads-not-suck&quot;&gt;Making Reads not Suck&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;We spent awhile brainstorming “how to make our reads not suck”, specifically avoiding manually updating all our endpoints’ SQL queries/business logic to do tedious/error-prone “remember to (maybe) do version-aware reads”.&lt;/p&gt;
&lt;p&gt;If you remember back to that screenshot from the beginning, we need our whole app/UI, at the flip of a dropdown, to automatically change every &lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt; query from:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- simple CRUD query&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; authors &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; id &lt;/span&gt;&lt;span&gt;IN&lt;/span&gt;&lt;span&gt; (?) &lt;/span&gt;&lt;span&gt;-- or whatever WHERE clause&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;To a “version-aware replacement”:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- find the right versions row&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; author_versions av&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; (?) &lt;/span&gt;&lt;span&gt;-- same WHERE clause&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;=&lt;/span&gt;&lt;span&gt; v10 &lt;/span&gt;&lt;span&gt;-- and versioning&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NULL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; v10)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; v10)&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;And not only for top-level &lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt;s but anytime &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt; is used in a query, i.e. in &lt;code dir=&quot;auto&quot;&gt;JOIN&lt;/code&gt;s:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- simple CRUD query that joins into `authors`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; b.&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; books&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;JOIN&lt;/span&gt;&lt;span&gt; authors a &lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;bob&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- version-aware replacement, uses books_versions *and* author_versions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- for the join &amp;#x26; where clause&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; bv.&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; book_versions bv&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- get the right author version&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;JOIN&lt;/span&gt;&lt;span&gt; author_versions av &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;bv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;=&lt;/span&gt;&lt;span&gt; v10 &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NULL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; v10))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- get the right book version&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;=&lt;/span&gt;&lt;span&gt; v10 &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;bv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NULL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; v10)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- predicate should use the version table&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;bob&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; v10))  -- get the right book version  WHERE bv.first &lt;= v10 AND (bv.final IS NULL or bv.final &gt; v10)  -- predicate should use the version table  AND av.first_name = &amp;#x27;bob&amp;#x27;;&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;1-initial-idea-using-ctes&quot;&gt;1. Initial Idea: Using CTEs&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;When it’s articulated like “we want &lt;em&gt;every table access to routed to the versions table&lt;/em&gt;”, a potential solution starts to emerge…&lt;/p&gt;
&lt;p&gt;Ideally we want to magically “swap out” &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt; with “a virtual &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt; table” that automatically has the right version-aware values. How could we do this, as easily as possible?&lt;/p&gt;
&lt;p&gt;It turns out a CTE is a great way of structuring this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- create the fake/version-aware authors table&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;WITH&lt;/span&gt;&lt;span&gt; _authors (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;-- make the columns look exactly like the regular table&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; id,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; first_name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;-- but read the data from author_versions behind the scenes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; author_versions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; (...&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt; matches...)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- now the rest of the application&apos;s SQL query as normal, but we swap out&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- the `authors` table with our `_authors` CTE&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; _authors a&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;bob&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;And that’s (almost) it!&lt;/p&gt;
&lt;p&gt;If anytime our app wants to “read authors”, regardless of the SQL query it’s making, we swapped &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt;-the-table to &lt;code dir=&quot;auto&quot;&gt;_authors&lt;/code&gt;-the-CTE, the SQL query would “for free” be using/returning the right version-aware values.&lt;/p&gt;
&lt;p&gt;So far this is just a prototype; we have three things left to do:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add the request-specific versioning config to the query&lt;/li&gt;
&lt;li&gt;Inject this rewriting as seamlessly as possible&lt;/li&gt;
&lt;li&gt;Evaluate the performance impact&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h3 id=&quot;2-adding-request-config-via-ctes&quot;&gt;2. Adding Request Config via CTEs&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;We have a good start, in terms of a hard-coded prototype SQL query, but now we need to get the &lt;code dir=&quot;auto&quot;&gt;first &amp;#x3C;= v10 AND ...&lt;/code&gt; pseudo code in the previous SQL snippets actually working.&lt;/p&gt;
&lt;p&gt;Instead of a hard-coded &lt;code dir=&quot;auto&quot;&gt;v10&lt;/code&gt;, we need queries to use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dynamic request-specific versioning (i.e. the user is currently looking at, or “pinning to”, &lt;code dir=&quot;auto&quot;&gt;PlanPackage&lt;/code&gt; v10), and&lt;/li&gt;
&lt;li&gt;Support pinning multiple different aggregate roots in the same request (i.e. the user is looking at &lt;code dir=&quot;auto&quot;&gt;PlanPackage&lt;/code&gt; v10 but &lt;code dir=&quot;auto&quot;&gt;DesignPackage&lt;/code&gt; v15)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CTEs are our new hammer 🔨—let’s add another for this, calling it &lt;code dir=&quot;auto&quot;&gt;_versions&lt;/code&gt; and using the &lt;code dir=&quot;auto&quot;&gt;VALUES&lt;/code&gt; syntax to synthesize a table:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;WITH&lt;/span&gt;&lt;span&gt; _versions (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; plan_id, version_id &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;-- parameters added to the query, one &quot;row&quot; per pinned aggregate&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;-- i.e. `[1, 10, 2, 15]` means this request wants `plan1=v10,plan2=v15`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; (?, ?), (?, ?)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;), _authors (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- the CTE from before but joins into _versions for versioning config&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; id,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; first_name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; authors a&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;JOIN&lt;/span&gt;&lt;span&gt; _versions v &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;plan_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;v&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;plan_id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- now we know:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- a) what plan the author belongs to (a.plan_id, i.e. its aggregate root)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- b) what version of the plan we&apos;re pinned to (v.version_id)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- so we can use them in our JOIN clause&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;JOIN&lt;/span&gt;&lt;span&gt; author_versions av &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;v&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;version_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;final_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NULL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;OR&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;final_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;v&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;version_id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; _authors a &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;bob&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt;= v.version_id AND (      av.final_id IS NULL OR av.final_id &lt; v.version_id    )  ))SELECT * FROM _authors a WHERE a.first_name = &amp;#x27;bob&amp;#x27;&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Now our application can “pass in the config” (i.e. for this request, &lt;code dir=&quot;auto&quot;&gt;plan:1&lt;/code&gt; uses &lt;code dir=&quot;auto&quot;&gt;v10&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;plan:2&lt;/code&gt; uses &lt;code dir=&quot;auto&quot;&gt;v15&lt;/code&gt;) as extra query parameters into the query, they’ll be added as rows to the &lt;code dir=&quot;auto&quot;&gt;_versions&lt;/code&gt; CTE table, and then the rest of the query will resolve versions using that data.&lt;/p&gt;
&lt;p&gt;Getting closer!&lt;/p&gt;
&lt;aside aria-label=&quot;Tip&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Tip&lt;/p&gt;&lt;div&gt;&lt;p&gt;It’s easy to miss, but a core aspect of our approach is that each row in the database “knows its parent aggregate root”. Because the core version config is “per package” (the aggregate root), but there are many child tables that we’ll be reading from, we use a strong/required convention that every child table must have a &lt;code dir=&quot;auto&quot;&gt;plan_id&lt;/code&gt; (or &lt;code dir=&quot;auto&quot;&gt;parent_id&lt;/code&gt;) foreign key that lets us join directly from the child to the parent’s version config.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;One issue with the query so far is that we must ahead-of-time “pin” every plan we want to read (by adding it to our &lt;code dir=&quot;auto&quot;&gt;_versions&lt;/code&gt; config table), b/c if a plan doesn’t have a row in the &lt;code dir=&quot;auto&quot;&gt;_versions&lt;/code&gt; CTE, then the &lt;code dir=&quot;auto&quot;&gt;INNER JOIN&lt;/code&gt;s will not find any &lt;code dir=&quot;auto&quot;&gt;p.version_id&lt;/code&gt; available, and none of its data will match.&lt;/p&gt;
&lt;p&gt;Ideally, any plan that is not explicitly pinned should have all its child entities read their active/published data (which will actually be in their &lt;code dir=&quot;auto&quot;&gt;author_versions&lt;/code&gt; tables, and not the primary &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt; tables); which we can do with (…wait for it…) another CTE:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;WITH&lt;/span&gt;&lt;span&gt; _versions (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- the injected config _versions stays the same as before&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; plan_id, version_id &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; (?, ?), (?, ?)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;), _plan_versions (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;-- we add an additional CTE that defaults all plans to active unless pinned in _versions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;plans&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; plan_id,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;-- prefer `v.version_id` but fallback on `active_version_id`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;COALESCE&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;v&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;version_id&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;plans&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;active_version_id&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; version_id,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; plans&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;LEFT OUTER JOIN&lt;/span&gt;&lt;span&gt; _versions v &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;v&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;plan_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;plans&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;_authors (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- this now joins on _plan_versions instead _versions directly&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; id,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; first_name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; authors a&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;JOIN&lt;/span&gt;&lt;span&gt; _plan_versions pv &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;plan_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;JOIN&lt;/span&gt;&lt;span&gt; author_versions av &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;version_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;final_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NULL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;OR&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;final_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;version_id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- automatically returns &amp;#x26; filters against the versioned data&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; _authors a &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;bob&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt;= pv.version_id AND (      av.final_id IS NULL OR av.final_id &lt; pv.version_id    )  ))-- automatically returns &amp;#x26; filters against the versioned dataSELECT * FROM _authors a WHERE a.first_name = &amp;#x27;bob&amp;#x27;&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;We’ve basically got it, in terms of a working prototype—now we just need to drop it into our application code, ideally as easily as possible.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;3-injecting-the-rewrite-automatically&quot;&gt;3. Injecting the Rewrite Automatically&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;We’ve discovered a scheme to make reads &lt;em&gt;automatically version-aware&lt;/em&gt;—now we want our application to use it, basically all the time, without us messing up or forgetting the rewrite incantation.&lt;/p&gt;
&lt;p&gt;Given that a) we completely messed this up the 1st time around 😕, and b) this seems like a very mechanical translation 🤖, &lt;strong&gt;what if we could automate it?&lt;/strong&gt; For every read?&lt;/p&gt;
&lt;p&gt;Our application already does all reads through Joist (of course 😅), as &lt;code dir=&quot;auto&quot;&gt;EntityManager&lt;/code&gt; calls:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Becomes SELECT * FROM authors WHERE id = 1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;em&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;load&lt;/span&gt;&lt;span&gt;(Author&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;a:1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Becomes SELECT * FROM authors WHERE first_name = ?&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;em&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt;(Author&lt;/span&gt;&lt;span&gt;, { firstName: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;bob&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Also becomes SELECT * FROM authors WHERE id = ?&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;book&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;load&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;It would be really great if these &lt;code dir=&quot;auto&quot;&gt;em.load&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; SQL queries were all magically rewritten—so that’s what we did. 🪄&lt;/p&gt;
&lt;p&gt;We built a Joist plugin that intercepts all &lt;code dir=&quot;auto&quot;&gt;em.load&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; queries (in a new plugin hook called &lt;code dir=&quot;auto&quot;&gt;beforeFind&lt;/code&gt;), and rewrites the query’s ASTs to be version-aware, before they are turned into SQL and sent to the database.&lt;/p&gt;
&lt;p&gt;So now what our endpoint/GraphQL query code does is:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;planPackageScopeLinesQuery&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// At the start of any version-aware endpoint, setup our VersioningPlugin...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VersioningPlugin&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Read the REST/GQL params to know which versions to pin&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const { &lt;/span&gt;&lt;span&gt;packageId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;versionId&lt;/span&gt;&lt;span&gt; } = &lt;/span&gt;&lt;span&gt;args;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pin&lt;/span&gt;&lt;span&gt;(packageId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; versionId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Install the plugin into the EM, for all future em.load/find calls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;em&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addPlugin&lt;/span&gt;&lt;span&gt;(plugin);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Now just use the em as normal and all operations will automatically&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// be tranlated into the `...from _versions...` SQL&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const { &lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt; } = &lt;/span&gt;&lt;span&gt;args;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; em&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt;(PlanPackageScopeLine&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// apply filter logic as normal, pseudo-code...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;quantity: filter&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;quantity&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;How does this work?&lt;/p&gt;
&lt;p&gt;Internally, Joist parses the arguments of &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; into an AST, &lt;code dir=&quot;auto&quot;&gt;ParsedFindQuery&lt;/code&gt;, that is a very simplified AST/ADT version of a SQL query, i.e. the shape looks like:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// An AST/ADT for an `em.find` call that will become a SQL `SELECT`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; ParsedFindQuery &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// I.e. arrays of `a.*` or `a.first_name`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;selects&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ParsedSelect&lt;/span&gt;&lt;span&gt;[];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// I.e. the `authors` table, with its alias, &amp;#x26; any inner/outer joins&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;tables&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ParsedTable&lt;/span&gt;&lt;span&gt;[];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// I.e. `WHERE` clauses&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;condition&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ParsedExpressionFilter&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;groupBys&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ParsedGroupBy&lt;/span&gt;&lt;span&gt;[];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;orderBys&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ParsedOrderBy&lt;/span&gt;&lt;span&gt;[];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ctes&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ParsedCteClause&lt;/span&gt;&lt;span&gt;[];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;After Joist takes the user’s “fluent DSL” input to &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; and parses it into this &lt;code dir=&quot;auto&quot;&gt;ParsedFindQuery&lt;/code&gt; AST, plugins can now inspect &amp;#x26; modify the query:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;VersioningPlugin&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// List of pinned plan=version tuples, populated per request&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;#versions &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; []&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Called for any em.load or em.find call&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;beforeFind&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EntityMetadata&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;query&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;ParsedFindQuery&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;let&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;didRewrite&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;table&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;of&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;query&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;tables&lt;/span&gt;&lt;span&gt;]) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;// Only rewrite tables that are versioned&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;hasVersionsTable&lt;/span&gt;&lt;span&gt;(table)) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;#rewriteTable&lt;/span&gt;&lt;span&gt;(query, table);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;didRewrite &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Only need to add these once/query&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (didRewrite) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;#addCTEs&lt;/span&gt;&lt;span&gt;(query);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;#&lt;/span&gt;&lt;span&gt;rewriteTable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;query&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;table&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// this `table` will initially be the `FROM authors AS a` from a query;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// leave the alias as-is, but swap the table from &quot;just `authors`&quot; to&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;//  the `_authors` CTE we&apos;ll add later&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;table&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;table&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;table&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;table&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// If `table.table=author`, get the AuthorMetadata that knows the columns&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;getMetadatFromTableName&lt;/span&gt;&lt;span&gt;(table&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;table&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Now inject the `_authors` CTE that to be our virtual table&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;query&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ctes&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;alias: &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;table&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;table&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;columns: [&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;query: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;kind: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;raw&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// put our &quot;read the right author_versions row&quot; query here; in this blog&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// post it is hard-coded to `authors` but in the real code would get&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// dynamically created based on the `meta` metadta&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sql: &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;av.author_id as id,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;av.first_name as first_name,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-- all other author columns...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;FROM authors a&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;JOIN _plan_versions pv ON a.plan_id = pv.id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;JOIN author_versions av ON (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;av.author_id = a.id AND&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;av.first_id &gt;= pv.version_id AND (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;av.final_id IS NULL OR av.final_id &amp;#x3C; pv.version_id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Inject the _versions and _plan_versions CTEs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;#&lt;/span&gt;&lt;span&gt;addCTEs&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;query&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;query&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ctes&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;( {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;alias: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;_versions&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;columns: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ columnName: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;plan_id&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, dbType: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ columnName: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;version_id&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, dbType: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;query: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;kind: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;raw&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bindings: &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;#versions&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;flatMap&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;pId&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;versionId&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;unsafeDeTagIds&lt;/span&gt;&lt;span&gt;([pId, versionId])),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sql: &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;SELECT * FROM (VALUES &lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;#versions&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;(?::int,?::int)&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;) AS t (plan_id, version_id)&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Do the same thing for _plan_versions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;query&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ctes&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt;= pv.version_id AND (              av.final_id IS NULL OR av.final_id &lt; pv.version_id            )          )        &amp;#x60;      }    })  }  // Inject the _versions and _plan_versions CTEs  #addCTEs(query) {    query.ctes.push( {      alias: &amp;#x22;_versions&amp;#x22;,      columns: [        { columnName: &amp;#x22;plan_id&amp;#x22;, dbType: &amp;#x22;int&amp;#x22; },        { columnName: &amp;#x22;version_id&amp;#x22;, dbType: &amp;#x22;int&amp;#x22; },      ],      query: {        kind: &amp;#x22;raw&amp;#x22;,        bindings: this.#versions.flatMap(([pId, versionId]) =&gt; unsafeDeTagIds([pId, versionId])),        sql: &amp;#x60;SELECT * FROM (VALUES ${this.#versions.map(() =&gt; &amp;#x22;(?::int,?::int)&amp;#x22;).join(&amp;#x22;,&amp;#x22;)}) AS t (plan_id, version_id)&amp;#x60;,      },    },    // Do the same thing for _plan_versions    query.ctes.push(...);  }}&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This is very high-level pseudo-code but the gist is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We’ve mutated the query to use our &lt;code dir=&quot;auto&quot;&gt;_authors&lt;/code&gt; CTE instead of the &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;We’ve injected the &lt;code dir=&quot;auto&quot;&gt;_authors&lt;/code&gt; CTE, creating it dynamically based on the &lt;code dir=&quot;auto&quot;&gt;AuthorMetadata&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;We’ve injected our &lt;code dir=&quot;auto&quot;&gt;_versions&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;_plan_versions&lt;/code&gt; config tables&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After the plugin’s &lt;code dir=&quot;auto&quot;&gt;beforeFind&lt;/code&gt; is finished, Joist takes the updated &lt;code dir=&quot;auto&quot;&gt;query&lt;/code&gt; and turns it into SQL, just like it would any &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; query, but now the SQL it generates &lt;em&gt;automatically reads the right versioned values&lt;/em&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;4-performance-evaluation&quot;&gt;4. Performance Evaluation&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Now that we have everything working, how was the performance?&lt;/p&gt;
&lt;p&gt;It was surprisingly good, but not perfect—we unfortunately saw a regression for reads “going through the CTE”, particularly when doing filtering, like:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;WITH&lt;/span&gt;&lt;span&gt; _versions (...),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;_plan_versions (...),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;_authors (...),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- this first_name is evaluating against the CTE results&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; _authors a &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;bob&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;We were disappointed b/c we thought since our &lt;code dir=&quot;auto&quot;&gt;_authors&lt;/code&gt; CTE is “only used once” in the SQL query, that ideally the PG query planner would essentially “inline” the CTE and pretend it was not even there, for planning &amp;#x26; indexing purposes.&lt;/p&gt;
&lt;p&gt;Contrast this with a CTE that is “used twice” in a SQL query, which our understanding is that then Postgres executes it once, and materializes it in memory (basically caches it, instead of executing it twice). This materialization would be fine for smaller CTEs like &lt;code dir=&quot;auto&quot;&gt;_versions&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;_plan_versions&lt;/code&gt;, but on a potentially huge table like &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;plan_package_scope_lines&lt;/code&gt;, we definitely don’t want those entire tables sequentially scanned and creating “versioned copies” materialized in-memory before any &lt;code dir=&quot;auto&quot;&gt;WHERE&lt;/code&gt; clauses were applied.&lt;/p&gt;
&lt;p&gt;So we thought our “only used once” &lt;code dir=&quot;auto&quot;&gt;_authors&lt;/code&gt; CTE rewrite would be performance neutral, but it was not—we assume because many of the CTE’s columns are not straight mappings, but due to some nuances with handling drafts, ended up being non-trivial &lt;code dir=&quot;auto&quot;&gt;CASE&lt;/code&gt; statements that look like:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- example of the rewritten select clauses in the `_authors` CTE&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; id,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;created_at&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; created_at,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- immutable columns&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;type_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; type_id,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- versioned columns&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CASE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WHEN&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_a_version&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NULL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;THEN&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_name&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;ELSE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_a_version&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;END&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; first_name,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CASE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WHEN&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_a_version&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NULL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;THEN&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;last_name&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;ELSE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_a_version&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;last_name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;END&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; last_name,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;And we suspected these &lt;code dir=&quot;auto&quot;&gt;CASE&lt;/code&gt; statements were not easy/possible for the query planner to “see through” and push filtering &amp;#x26; indexing statistics through the top-level &lt;code dir=&quot;auto&quot;&gt;WHERE&lt;/code&gt; clause.&lt;/p&gt;
&lt;p&gt;So, while so far our approach has been “add yet another CTE”, for this last stretch, we had to remove the &lt;code dir=&quot;auto&quot;&gt;_authors&lt;/code&gt; CTE and start “hard mode” rewriting the query by adding &lt;code dir=&quot;auto&quot;&gt;JOIN&lt;/code&gt;s directly to the query itself, i.e. we’d go from a non-versioned query like:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- previously we&apos;d &quot;just swap&quot; the books &amp;#x26; authors tables to&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- versioned _books &amp;#x26; _authors CTEs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; b.&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; books&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;JOIN&lt;/span&gt;&lt;span&gt; authors a &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;bob&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;To:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- rewrite the top-level select&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; id,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- any versioned columns need `CASE` statements&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CASE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WHEN&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NULL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;THEN&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ELSE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; title,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CASE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WHEN&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NULL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;THEN&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;notes&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ELSE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;notes&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; notes,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- ...repeat for each versioned column...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- ...also special for updated_at...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; books&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- keep the `books` table &amp;#x26; add a `book_versions` JOIN directly to the query&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;JOIN&lt;/span&gt;&lt;span&gt; book_versions bv &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;bv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;book_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;version_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; ...)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- rewrite the `ON` to use `bv.author_id` (b/c books can change authors)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;JOIN&lt;/span&gt;&lt;span&gt; authors a &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- add a author_version join directly to the query&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;JOIN&lt;/span&gt;&lt;span&gt; author_versions av &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;version_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; ...)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-- rewrite the condition from `a.first_name` to `av.first_name`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;av&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;first_name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;bob&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt;= pv.version_id AND ...)  -- rewrite the &amp;#x60;ON&amp;#x60; to use &amp;#x60;bv.author_id&amp;#x60; (b/c books can change authors)  JOIN authors a ON bv.author_id = a.id  -- add a author_version join directly to the query  JOIN author_versions av ON (av.author_id = a.id AND av.first_id &gt;= pv.version_id AND ...)  -- rewrite the condition from &amp;#x60;a.first_name&amp;#x60; to &amp;#x60;av.first_name&amp;#x60;  WHERE av.first_name = &amp;#x27;bob&amp;#x27;;&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This is a lot more rewriting!&lt;/p&gt;
&lt;p&gt;While the CTE approach let us just “swap the table”, and leave the rest of the query “reading from the alias &lt;code dir=&quot;auto&quot;&gt;a&lt;/code&gt;” &amp;#x26; generally being none-the-wiser, now we have to find every &lt;code dir=&quot;auto&quot;&gt;b&lt;/code&gt; alias usage or &lt;code dir=&quot;auto&quot;&gt;a&lt;/code&gt; alias usage, and evaluate if the &lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;JOIN ON&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;WHERE&lt;/code&gt; clause is touching a versioned column, and if so rewrite that usage to the &lt;code dir=&quot;auto&quot;&gt;bv&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;av&lt;/code&gt; respective versioned column.&lt;/p&gt;
&lt;p&gt;There are pros/cons to this approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Con: Obviously the query is much trickier to rewrite&lt;/li&gt;
&lt;li&gt;Pro: But since our rewriting algorithm is isolated to the &lt;code dir=&quot;auto&quot;&gt;VersioningPlugin&lt;/code&gt; file, we were able to “refactor our versioned query logic” just once and have it &lt;em&gt;apply everywhere&lt;/em&gt; which was amazing 🎉&lt;/li&gt;
&lt;li&gt;Pro: The “more complicated to us” CTE-less query is actually “simpler to the Postgres query planner” b/c there isn’t a CTE “sitting in the way” and so all the usual indexing/filtering performance optimizations kicked in, and got us back to baseline performance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Removing the &lt;code dir=&quot;auto&quot;&gt;_authors&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;_books&lt;/code&gt; CTEs and doing “inline rewriting” (basically what we’d hoped Postgres would do for us with the “used once” CTEs, but now we’re doing by hand) gave us a ~10x performance increase, and returned us to baseline performance, actually beating the performance of our original “write to drafts” approach. 🏃&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;skipping-some-details&quot;&gt;Skipping Some Details&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;It would make the post even longer, so I’m skipping some of the nitty-gritty details like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Soft deletions—should entities “disappear” if they were added in v10 and the user pins to v8? Or “disappear” if they were deleted in v15 and the user is looking at v20?
&lt;ul&gt;
&lt;li&gt;Initially we had our &lt;code dir=&quot;auto&quot;&gt;VersionPlugin&lt;/code&gt; plugin auto-filter these rows, but in practice this was too strict for some of our legacy code paths, so in both “not yet added” and “previously deleted” scenarios, we return rows anyway &amp;#x26; then defer to application-level filtering&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Versioning &lt;code dir=&quot;auto&quot;&gt;m2m&lt;/code&gt; collections, both in the database (store full copies or incremental diffs?), and teaching the plugin to rewrite m2m joins/filters accordingly.&lt;/li&gt;
&lt;li&gt;Reading &lt;code dir=&quot;auto&quot;&gt;updated_at&lt;/code&gt; from the right identity table vs. the versions table to avoid oplock errors when drafts issue &lt;code dir=&quot;auto&quot;&gt;UPDATE&lt;/code&gt;s using plugin-loaded data&lt;/li&gt;
&lt;li&gt;Ensuring endpoints make the &lt;code dir=&quot;auto&quot;&gt;pin&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;addPlugin&lt;/code&gt; calls without accidentally loading “not versioned” copies of the data they want to read into the &lt;code dir=&quot;auto&quot;&gt;EntityManager&lt;/code&gt;, which would cache the non-versioned data, &amp;#x26; prevent future “should be versioned” reads for working as expected.&lt;/li&gt;
&lt;li&gt;Migrating our codebase from the previous “by hand” / “write to drafts” initial versioning approach, to the new plugin + “write to identities” approach, which honestly was a lot of fun—lots of red code that was deleted &amp;#x26; simplified by the new approach. 🔪&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thankfully we were able to solve each of these, and none turned into dealbreakers that compromised the overall approach. 😅&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This was definitely a long-form post, as we explored the Homebound problem space that drove our solution, rather than just a shorter announcement post of “btw Joist now has a plugin API”.&lt;/p&gt;
&lt;p&gt;Which, yes, Joist does now have a plugin API for query rewriting 🎉, but we think it’s important to show how/why it was useful to us, and potentially inspire ideas for how it might be useful to others as well (i.e. an auth plugin that does ORM/data layer auth 🔑 is also on our todo list).&lt;/p&gt;
&lt;p&gt;That said, we anticipate readers wondering “wow this solution seems too complex” (and, yes, our production &lt;code dir=&quot;auto&quot;&gt;VersionPlugin&lt;/code&gt; code is much more complicated than the pseudocode we’ve used in this post), “why didn’t you hand-write the queries”, etc 😰.&lt;/p&gt;
&lt;p&gt;We can only report that we tried “just hand-write your versioning queries”, in the spirit of KISS &amp;#x26; moving quickly while building our initial set of version-aware features, for about 6-9 months, and it was terrible. 😢&lt;/p&gt;
&lt;p&gt;Today, we have versioning implemented as “a cross-cutting concern” (anyone remember &lt;a href=&quot;https://en.wikipedia.org/wiki/Aspect-oriented_programming&quot;&gt;Aspect Oriented Programming&lt;/a&gt;? 👴), primarily isolated to a single file/plugin, and the rest of our code went back to “boring CRUD” with “boring reads” and “boring writes”.&lt;/p&gt;
&lt;p&gt;Our velocity has increased, bugs have decreased, and overall DX/developer happiness is back to our usual “this is a pleasant codebase” levels. 🎉&lt;/p&gt;
&lt;p&gt;If you have any questions, feel free to drop by our Discord to chat.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;thanks&quot;&gt;Thanks&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Thanks to the Homebound engineers who worked on this project: Arvin, for bearing the brunt of the tears &amp;#x26; suffering, fixing bugs during our pre-plugin “write to drafts” approach (mea cupla! 😅), ZachG for owning the rewriting plugin, both Joist’s new plugin API &amp;#x26; our internal &lt;code dir=&quot;auto&quot;&gt;VersioningPlugin&lt;/code&gt; implementation 🚀, and Roberth, Allan, and ZachO for all pitching in to get our refactoring landed in the limited, time-boxed window we had for the initiative ⏰🎉.&lt;/p&gt;</content:encoded></item><item><title>Lazy Fields for 30x speedup without Decorators or Transforms</title><link>https://joist-orm.io/blog/lazy-fields/</link><guid isPermaLink="true">https://joist-orm.io/blog/lazy-fields/</guid><description>Joist leverages JavaScript&apos;s prototypes to blend great developer ergonomics/DX with performance.

</description><pubDate>Mon, 06 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Joist is an &lt;a href=&quot;https://guides.rubyonrails.org/active_record_basics.html&quot;&gt;ActiveRecord&lt;/a&gt;-style TypeScript ORM, such that we purposefully mimic how Rails declares relations, i.e. as class fields:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// simplified example&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Author&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;books &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hasMany&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;books&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;These class fields are very ergonomic, i.e. no decorators (which we &lt;a href=&quot;https://joist-orm.io/blog/avoiding-decorators/&quot;&gt;purposefully avoid&lt;/a&gt;) or other boilerplate, so much so that at &lt;a href=&quot;https://www.homebound.com/&quot;&gt;Homebound&lt;/a&gt;, we’ve written (or codegen-d) literally thousands of these class fields in our domain models.&lt;/p&gt;
&lt;aside aria-label=&quot;Tip&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Tip&lt;/p&gt;&lt;div&gt;&lt;p&gt;Unlike query-builder ORMs like Prisma, Joist’s class fields can be incrementally populated (and cached!), similar to TypeORM, except that, unlike TypeORM, Joist also tracks loaded-ness in the &lt;a href=&quot;https://joist-orm.io/goals/load-safe-relations&quot;&gt;type system&lt;/a&gt;, resolving one of its biggest footguns.&lt;/p&gt;&lt;p&gt;This allows business logic to dynamically/iteratively load data as-needed for its computation (but &lt;a href=&quot;https://joist-orm.io/goals/avoiding-n-plus-1s/&quot;&gt;without N+1s&lt;/a&gt;!), instead of trying to “one-shot” load it as part of a single large up-front query.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;fields-are-expensive-at-scale&quot;&gt;Fields are Expensive at Scale&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This approach has worked really well, with the only downside being that JavaScript’s class fields are eagerly initialized.&lt;/p&gt;
&lt;p&gt;This means when &lt;code dir=&quot;auto&quot;&gt;new Author&lt;/code&gt; is called, all the fields in &lt;code dir=&quot;auto&quot;&gt;class Author&lt;/code&gt;, like &lt;code dir=&quot;auto&quot;&gt;books = hasMany(...)&lt;/code&gt; above, and any others like &lt;code dir=&quot;auto&quot;&gt;publisher = hasOne(...)&lt;/code&gt;, etc., are immediately created &amp;#x26; allocated in memory.&lt;/p&gt;
&lt;p&gt;These class fields are created &amp;#x26; assigned &lt;em&gt;every time&lt;/em&gt;, for &lt;em&gt;every author&lt;/em&gt; instance, even if the code that called &lt;code dir=&quot;auto&quot;&gt;new Author&lt;/code&gt; doesn’t end up using the &lt;code dir=&quot;auto&quot;&gt;books&lt;/code&gt; relation (maybe it only uses &lt;code dir=&quot;auto&quot;&gt;publisher&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;This eager-ness is fine when &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; has a handful of relations, but in sufficiently-complicated domain models, some of our core entities have 30 relations, and the cost of creating 30 relations &lt;code dir=&quot;auto&quot;&gt;x&lt;/code&gt; 1,000 of rows can start to add up.&lt;/p&gt;
&lt;p&gt;So our goal is to keep the &lt;code dir=&quot;auto&quot;&gt;books = hasMany(&quot;books&quot;)&lt;/code&gt; syntax, but somehow make the fields lazy, so we only pay for what we use.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;first-approach-codegen&quot;&gt;First Approach: Codegen&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Not surprisingly, our first solution was to leverage code generation.&lt;/p&gt;
&lt;p&gt;For an application’s default, foreign-key-based relations like one-to-many, many-to-one, etc., Joist already generates a &lt;code dir=&quot;auto&quot;&gt;AuthorCodegen&lt;/code&gt; class that has the boilerplate relations defined for free.&lt;/p&gt;
&lt;p&gt;I.e. in Joist, you get getters &amp;#x26; setters like &lt;code dir=&quot;auto&quot;&gt;author.firstName&lt;/code&gt;, and relations like &lt;code dir=&quot;auto&quot;&gt;author.books&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;publisher.author&lt;/code&gt; all for free, from the generated &lt;code dir=&quot;auto&quot;&gt;AuthorCodegen&lt;/code&gt; base class.&lt;/p&gt;
&lt;p&gt;Because these &lt;code dir=&quot;auto&quot;&gt;...Codegen&lt;/code&gt; classes are not handwritten, it was easy to just swap the field-based code over to getters:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// in AuthorCodegen.ts, generated by joist-codegen&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AuthorCodegen&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// lazy cache of relations&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;#relations&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Record&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Not a field anymore, but looks like one to callers&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;books&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;OneToMany&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Book&lt;/span&gt;&lt;span&gt;&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;#relations&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;books&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;??=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hasMany&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;books&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// in Author.ts, written by the user&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Author&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AuthorCodegen&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// we get the lazy relations for free&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; = {};  // Not a field anymore, but looks like one to callers  get books(): OneToMany&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;</content:encoded></item><item><title>Avoiding ORM Decorators</title><link>https://joist-orm.io/blog/avoiding-decorators/</link><guid isPermaLink="true">https://joist-orm.io/blog/avoiding-decorators/</guid><description>Entity-based ORMs, going back to Java&apos;s Hibernate &amp; earlier, often use decorators or annotations like @PrimaryKey to define the domain model; Joist pushes back on this pattern, and prefers schema-driven code generation.

</description><pubDate>Sun, 27 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Joist is an entity-based ORM, i.e. an &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt; table gets an &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; class, to hold business logic (both simple and &lt;a href=&quot;https://joist-orm.io/modeling/validation-rules/#reactive-validation-rules&quot;&gt;complex validation rules&lt;/a&gt;, &lt;a href=&quot;https://joist-orm.io/modeling/reactive-fields/&quot;&gt;reactive fields&lt;/a&gt;, &lt;a href=&quot;https://joist-orm.io/modeling/lifecycle-hooks/&quot;&gt;lifecycle hooks&lt;/a&gt;, etc.):&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Author.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Author&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AuthorCodegen&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Example of trivial business logic...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;fullName&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;firstName&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;lastName&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;It’s common for other entity-based ORMs to use decorators (in JavaScript/TypeScript, also called annotations in Java) to define the domain model itself, i.e. use a &lt;strong&gt;code-first approach&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For example, in MikroORM:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Author.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@Entity&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@PrimaryKey&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;!:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@Property&lt;/span&gt;&lt;span&gt;({ unique: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;!:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@Property&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;firstName&lt;/span&gt;&lt;span&gt;!:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@Property&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;lastName&lt;/span&gt;&lt;span&gt;!:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@OneToMany&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; Order, &lt;/span&gt;&lt;span&gt;order&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; order&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;orders &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Collection&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Order&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; Order, order =&gt; order.user)  orders = new Collection&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;</content:encoded></item><item><title>Pipelining for 3-6x Faster Commits</title><link>https://joist-orm.io/blog/initial-pipelining-benchmark/</link><guid isPermaLink="true">https://joist-orm.io/blog/initial-pipelining-benchmark/</guid><description>Pipelining INSERTs and UPDATEs statements to make commits 3-6x faster

</description><pubDate>Sun, 20 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve known about Postgres’s &lt;a href=&quot;https://www.postgresql.org/docs/current/libpq-pipeline-mode.html&quot;&gt;pipeline mode&lt;/a&gt; for a while, and finally have some prototyping of pipelining in general, and alpha builds of Joist running with pipeline mode (coming soon!).&lt;/p&gt;
&lt;p&gt;This post is an intro to pipelining, using &lt;a href=&quot;https://github.com/porsager/postgres&quot;&gt;postgres.js&lt;/a&gt; and &lt;a href=&quot;https://www.npmjs.com/package/mitata&quot;&gt;mitata&lt;/a&gt; to benchmark some examples.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-is-pipelining&quot;&gt;What is Pipelining?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Pipelining, as a term in networking, allows clients to send multiple requests, immediately one after each other, without first waiting for the server to respond.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;without-pipelining&quot;&gt;Without Pipelining&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Using NodeJS talking to Postgres for illustration, the default flow of SQL statements, without pipelining, involves a full round-trip network request for each SQL statement:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://joist-orm.io/pipelining-regular.jpg&quot; alt=&quot;Without Pipelining&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Send an &lt;code dir=&quot;auto&quot;&gt;INSERT authors&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;…wait several millis for work &amp;#x26; response…&lt;/li&gt;
&lt;li&gt;Send an &lt;code dir=&quot;auto&quot;&gt;INSERT books&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;…wait several millis for work &amp;#x26; response…&lt;/li&gt;
&lt;li&gt;Send an &lt;code dir=&quot;auto&quot;&gt;INSERT reviews&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;…wait several millis for work &amp;#x26; response…&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that we have to wait for &lt;em&gt;both&lt;/em&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The server to “complete the work” (maybe 1ms), and&lt;/li&gt;
&lt;li&gt;The network to deliver the responses back to us (maybe 2ms)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Before we can continue sending the next request.&lt;/p&gt;
&lt;p&gt;This results in a lot of “wait time”, for both the client &amp;#x26; server, while each is waiting for the network call of the other to transfer over the wire.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;with-pipelining&quot;&gt;With Pipelining&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Pipelining allows us to remove this “extra wait time” by sending all the requests at once, and then waiting for all responses:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://joist-orm.io/pipelining-pipelined.jpg&quot; alt=&quot;With Pipelining&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Send &lt;code dir=&quot;auto&quot;&gt;INSERT authors&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Send &lt;code dir=&quot;auto&quot;&gt;INSERT books&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Send &lt;code dir=&quot;auto&quot;&gt;INSERT reviews&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;…wait several millis for all 3 requests to complete…&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The upshot is that &lt;strong&gt;we’re not waiting on the network&lt;/strong&gt; before sending the server more work to do.&lt;/p&gt;
&lt;p&gt;Not only does this let our client “send work” sooner, but it lets the server have “work to do” sooner as well—i.e. as soon as the server finishes &lt;code dir=&quot;auto&quot;&gt;INSERT authors&lt;/code&gt;, it can immediately start working on &lt;code dir=&quot;auto&quot;&gt;INSERT books&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;transactions-required&quot;&gt;Transactions Required&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;One wrinkle with pipelining is that if 1 SQL statement fails (i.e. the &lt;code dir=&quot;auto&quot;&gt;INSERT authors&lt;/code&gt; statement), all requests that follow it in the pipeline are also aborted.&lt;/p&gt;
&lt;p&gt;This is because Postgres assumes the later statements in the pipeline relied on the earlier statements succeeding, so once earlier statements fail, the later statements are considered no longer valid.&lt;/p&gt;
&lt;p&gt;This generally means pipelining is only useful when executing multi-statement database transactions, where you’re executing a &lt;code dir=&quot;auto&quot;&gt;BEGIN&lt;/code&gt; + some number of &lt;code dir=&quot;auto&quot;&gt;INSERT&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;UPDATE&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;DELETE&lt;/code&gt; statements + &lt;code dir=&quot;auto&quot;&gt;COMMIT&lt;/code&gt;, and we already expect them to all atomically commit.&lt;/p&gt;
&lt;p&gt;Serendipitously, this model of “this group of statements all need to work or abort” is exactly what we want anyway for a single backend request that is committing its work, by atomically saving its work to the database in a transaction—and is exactly what Joist’s &lt;code dir=&quot;auto&quot;&gt;em.flush&lt;/code&gt; does. :-)&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;benchmarking-wire-latency&quot;&gt;Benchmarking Wire Latency&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Per above, network latency between your machine &amp;#x26; the database is the biggest factor in pipelining’s performance impact.&lt;/p&gt;
&lt;p&gt;This can make benchmarking difficult and potentially misleading, because benchmarks often have the “web backend” and “the database” on the same physical machine, which means there is effectively zero network latency.&lt;/p&gt;
&lt;p&gt;Thankfully, we can use solutions like Shopify’s &lt;a href=&quot;https://github.com/Shopify/toxiproxy&quot;&gt;toxiproxy&lt;/a&gt; to introduce an artificial, deterministic amount of latency to the network requests between our Node process and the Postgres database.&lt;/p&gt;
&lt;p&gt;toxiproxy is particularly neat in that it’s easy to run as a docker container, and control the latency via &lt;code dir=&quot;auto&quot;&gt;POST&lt;/code&gt; commands to a minimal REST API it exposes:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;docker-compose.yml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;services&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;toxiproxy&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;ghcr.io/shopify/toxiproxy:2.12.0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;depends_on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;condition&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;service_healthy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ports&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;5432:5432&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;8474:8474&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;volumes&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;./toxiproxy.json:/config/toxiproxy.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;command&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;-host=0.0.0.0 -config=/config/toxiproxy.json&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;toxiproxy.json&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;postgres&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;listen&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;0.0.0.0:5432&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;upstream&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;db:5432&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;enabled&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;toxi-init.sh&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-X&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;http://localhost:8474/reset&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-X&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;http://localhost:8474/proxies/postgres/toxics&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-d&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;name&quot;: &quot;latency_downstream&quot;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;type&quot;: &quot;latency&quot;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;stream&quot;: &quot;downstream&quot;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;attributes&quot;: { &quot;latency&quot;: 2 }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Is all we need to control exactly how much latency toxiproxy injects between every Node.js database call &amp;#x26; our docker-hosted postgres instance.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;leveraging-postgresjs&quot;&gt;Leveraging postgres.js&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;We’ll look at Joist’s pipeline performance in a future post, but for now we’ll stay closer to the metal and use &lt;a href=&quot;https://github.com/porsager/postgres&quot;&gt;postgres.js&lt;/a&gt; to directly execute SQL statements in a few benchmarks.&lt;/p&gt;
&lt;p&gt;We’re using postgres.js instead of the venerable node-pg solely because postgres.js implements pipelining, but node-pg does not yet.&lt;/p&gt;
&lt;p&gt;postgres.js also has an extremely seamless way to use pipelining—any statements issued in parallel (i.e. a &lt;code dir=&quot;auto&quot;&gt;Promise.all&lt;/code&gt;) within a &lt;code dir=&quot;auto&quot;&gt;sql.begin&lt;/code&gt; are automatically pipelined for us.&lt;/p&gt;
&lt;p&gt;Very neat!&lt;/p&gt;
&lt;aside aria-label=&quot;Info&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Info&lt;/p&gt;&lt;div&gt;&lt;p&gt;The Postgres &lt;a href=&quot;https://www.postgresql.org/docs/current/libpq-pipeline-mode.html&quot;&gt;pipelining docs&lt;/a&gt; make a valid point that pipelining requires async behavior, which in traditional blocking languages like Java &amp;#x26; C, is a significant complexity increase, such that pipelining may not be worth the trade-off.&lt;/p&gt;&lt;p&gt;However, JavaScript is already async &amp;#x26; non-blocking, so submitting several requests in parallel, and waiting for them to return, is extremely natural; it’s just &lt;code dir=&quot;auto&quot;&gt;Promise.all&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Example of how easy/natural submitting parallel requests is in JS&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const [&lt;/span&gt;&lt;span&gt;response1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;response2&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;response3&lt;/span&gt;&lt;span&gt;] = await &lt;/span&gt;&lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;all&lt;/span&gt;&lt;span&gt;([&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;sendRequest1&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;sendRequest2&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;sendRequest3&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;]);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;benchmarks&quot;&gt;Benchmarks&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;0-setup&quot;&gt;0. Setup&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;We’ll use &lt;a href=&quot;https://www.npmjs.com/package/mitata&quot;&gt;mitata&lt;/a&gt; for timing info—it is technically focused on CPU micro-benchmarks, but its warmup &amp;#x26; other infra make it suitable to our async, I/O oriented benchmark as well.&lt;/p&gt;
&lt;p&gt;For SQL statements, we’ll test inserting &lt;code dir=&quot;auto&quot;&gt;tag&lt;/code&gt; rows into a single-column table—for these tests, the complexity/cost of the statement itself is not that important, and a simple insert will do.&lt;/p&gt;
&lt;p&gt;We have a few configuration parameters, that can be tweaked across runs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;numStatements&lt;/code&gt; the number of tags to insert&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;toxiLatencyInMillis&lt;/code&gt; the latency in millis that toxiproxy should delay each statement&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As we’ll see, both of these affect the results—the higher each becomes (the more statements, or the more latency), the more performance benefits we get from pipelining.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;1-sequential-inserts&quot;&gt;1. Sequential Inserts&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;As a baseline benchmark, we execute &lt;code dir=&quot;auto&quot;&gt;numStatements&lt;/code&gt; inserts sequentially, with individual &lt;code dir=&quot;auto&quot;&gt;await&lt;/code&gt;s on each &lt;code dir=&quot;auto&quot;&gt;INSERT&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;bench&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;sequential&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; sql&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;begin&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;let &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;; i &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; numStatements; i&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;INSERT INTO tag (name) VALUES (&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;value-&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;nextTag&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; {  await sql.begin(async (sql) =&gt; {    for (let i = 0; i &lt; numStatements; i++) {      await sql&amp;#x60;INSERT INTO tag (name) VALUES (${&amp;#x60;value-${nextTag++}&amp;#x60;})&amp;#x60;;    }  });});&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;We expect this to be the slowest, because it is purposefully defeating pipelining by waiting for each &lt;code dir=&quot;auto&quot;&gt;INSERT&lt;/code&gt; to finish before executing the next one.&lt;/p&gt;
&lt;aside aria-label=&quot;info&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;info&lt;/p&gt;&lt;div&gt;&lt;p&gt;Ideally your code, or ORM, would be inserting 10 tags as a single batch &lt;code dir=&quot;auto&quot;&gt;INSERT&lt;/code&gt;, as Joist does automatically.&lt;/p&gt;&lt;p&gt;But here we’re less concerned about what each specific SQL statement does, and more just how many statements we’re executing &amp;#x26; waiting for return values—so a non-batch &lt;code dir=&quot;auto&quot;&gt;INSERT&lt;/code&gt; into tags will suffice.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h3 id=&quot;2-pipelining-with-return-value&quot;&gt;2. Pipelining with return value&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;This is postgres.js’s canonical way of invoking pipelining, returning a &lt;code dir=&quot;auto&quot;&gt;string[]&lt;/code&gt; of SQL statements from the &lt;code dir=&quot;auto&quot;&gt;sql.begin&lt;/code&gt; lambda:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;bench&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;pipeline string[]&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; sql&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;begin&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;statements&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; [];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;let &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;; i &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; numStatements; i&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;statements&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;INSERT INTO tag (name) VALUES (&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;value-&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;nextTag&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; statements;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; {  await sql.begin((sql) =&gt; {    const statements = [];    for (let i = 0; i &lt; numStatements; i++) {      statements.push(sql&amp;#x60;INSERT INTO tag (name) VALUES (${&amp;#x60;value-${nextTag++}&amp;#x60;})&amp;#x60;);    }    return statements;  });});&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;We expect this to be fast, because of pipelining.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;3-pipelining-with-promiseall&quot;&gt;3. Pipelining with Promise.all&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;This last example also uses postgres.js’s pipelining, but by invoking the statements from within a &lt;code dir=&quot;auto&quot;&gt;Promise.all&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;bench&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;pipeline Promise.all&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; sql&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;begin&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;statements&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; [];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;let &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;; i &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; numStatements; i&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;statements&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;INSERT INTO tag (name) VALUES (&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;value-&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;nextTag&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;all&lt;/span&gt;&lt;span&gt;(statements);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; {  await sql.begin(async (sql) =&gt; {    const statements = [];    for (let i = 0; i &lt; numStatements; i++) {      statements.push(sql&amp;#x60;INSERT INTO tag (name) VALUES (${&amp;#x60;value-${nextTag++}&amp;#x60;})&amp;#x60;);    }    await Promise.all(statements);  });});&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This is particularly important for Joist, because even within a single &lt;code dir=&quot;auto&quot;&gt;em.flush()&lt;/code&gt; call, we’ll execute a single &lt;code dir=&quot;auto&quot;&gt;BEGIN&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;COMMIT&lt;/code&gt; database transaction, but potentially might have to make several “waves” of SQL updates (technically only when &lt;code dir=&quot;auto&quot;&gt;ReactiveQueryField&lt;/code&gt;s are involved), and so can’t always return a single &lt;code dir=&quot;auto&quot;&gt;string[]&lt;/code&gt; of SQL statements to execute.&lt;/p&gt;
&lt;p&gt;We expect this to be fast as well.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;performance-results&quot;&gt;Performance Results&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;I’ve run the benchmark with a series of latencies &amp;#x26; statements.&lt;/p&gt;
&lt;p&gt;1ms latency, 10 statements:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;toxiproxy configured with 1ms latency&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;numStatements 10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;clk: ~4.37 GHz&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cpu: Intel(R) Core(TM) i9-10885H CPU @ 2.40GHz&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;runtime: node 23.10.0 (x64-linux)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;benchmark                   avg (min … max)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-------------------------------------------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sequential                    15.80 ms/iter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pipeline string[]              4.16 ms/iter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pipeline Promise.all           4.21 ms/iter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;summary&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;pipeline string[]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1.01x faster than pipeline Promise.all&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3.8x faster than sequential&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;1ms latency, 20 statements:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;toxiproxy configured with 1ms latency&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;numStatements 20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;clk: ~4.52 GHz&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cpu: Intel(R) Core(TM) i9-10885H CPU @ 2.40GHz&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;runtime: node 23.10.0 (x64-linux)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;benchmark                   avg (min … max)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-------------------------------------------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sequential                    30.43 ms/iter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pipeline string[]              4.55 ms/iter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pipeline Promise.all           4.51 ms/iter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;summary&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;pipeline Promise.all&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1.01x faster than pipeline string[]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;6.74x faster than sequential&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;2ms latency, 10 statements:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;toxiproxy configured with 2ms latency&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;numStatements 10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;clk: ~4.53 GHz&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cpu: Intel(R) Core(TM) i9-10885H CPU @ 2.40GHz&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;runtime: node 23.10.0 (x64-linux)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;benchmark                   avg (min … max)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-------------------------------------------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sequential                    28.85 ms/iter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pipeline string[]              7.27 ms/iter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pipeline Promise.all           7.54 ms/iter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;summary&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;pipeline string[]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1.04x faster than pipeline Promise.all&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3.97x faster than sequential&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;2ms latency, 20 statements:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;toxiproxy configured with 2ms latency&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;numStatements 20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;clk: ~4.48 GHz&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cpu: Intel(R) Core(TM) i9-10885H CPU @ 2.40GHz&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;runtime: node 23.10.0 (x64-linux)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;benchmark                   avg (min … max)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-------------------------------------------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sequential                    55.05 ms/iter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pipeline string[]              9.17 ms/iter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pipeline Promise.all          10.13 ms/iter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;summary&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;pipeline string[]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1.1x faster than pipeline Promise.all&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;6x faster than sequential&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;So, in these benchmarks, pipelining makes our inserts (and ideally future Joist &lt;code dir=&quot;auto&quot;&gt;em.flush&lt;/code&gt; calls!) 3x to 6x faster.&lt;/p&gt;
&lt;p&gt;A few notes on these numbers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1-2ms latency I think is a generally correct/generous latency, based on what our production app sees between an Amazon ECS container and RDS Aurora instance.&lt;/p&gt;
&lt;p&gt;(Although if you’re using &lt;a href=&quot;https://gist.github.com/rxliuli/be31cbded41ef7eac6ae0da9070c8ef8#using-batch-requests&quot;&gt;edge-based compute&lt;/a&gt; this can be as high as 200ms :-O)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;10 statements per &lt;code dir=&quot;auto&quot;&gt;em.flush&lt;/code&gt; seems like a lot, but if you think about “each table that is touched”, whether due to an &lt;code dir=&quot;auto&quot;&gt;INSERT&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;UPDATE&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;DELETE&lt;/code&gt;, and include many-to-many tables, I think it’s reasonable for 10-tables to be a not-uncommon number.&lt;/p&gt;
&lt;p&gt;Note that we assume your SQL statements are already batched-per-table, i.e. if you have 10 author rows to &lt;code dir=&quot;auto&quot;&gt;UPDATE&lt;/code&gt;, you should be issuing a single &lt;code dir=&quot;auto&quot;&gt;UPDATE authors&lt;/code&gt; that batch-updates all 10 rows. If you’re using Joist, it already does this for you.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;pipelining-ftw&quot;&gt;Pipelining FTW&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;I created this raw SQL benchmark to better understand pipelining’s l-wlevel performance impact, and I think it’s an obvious win: &lt;strong&gt;3-6x speedups&lt;/strong&gt; in multi-statement transactions.&lt;/p&gt;
&lt;p&gt;As a reminder/summary, to leverage pipelining you need three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A postgresql driver that supports it,&lt;/li&gt;
&lt;li&gt;Be executing multi-statement transactions, and&lt;/li&gt;
&lt;li&gt;Structure your code such that all the transaction statements are submitted in parallel&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This last one is where Joist is the most helpful—it’s &lt;code dir=&quot;auto&quot;&gt;em.flush()&lt;/code&gt; method automatically generates the &lt;code dir=&quot;auto&quot;&gt;INSERT&lt;/code&gt;s, &lt;code dir=&quot;auto&quot;&gt;UPDATE&lt;/code&gt;s, and &lt;code dir=&quot;auto&quot;&gt;DELETE&lt;/code&gt;s for your changes, and so it can automatically submit them using a &lt;code dir=&quot;auto&quot;&gt;Promise.all&lt;/code&gt;, and not require any restructuring in your code.&lt;/p&gt;
&lt;p&gt;In a future/next post, we’ll swap these raw SQL benchmarks out for higher-level ORM benchmarks, to see pipelining’s impact in more realistic scenarios.&lt;/p&gt;
&lt;aside aria-label=&quot;info&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;info&lt;/p&gt;&lt;div&gt;&lt;p&gt;The code for this post is in &lt;a href=&quot;https://github.com/joist-orm/joist-benchmarks/blob/main/packages/benchmark/src/pipeline.ts&quot;&gt;pipeline.ts&lt;/a&gt; in the &lt;a href=&quot;https://github.com/joist-orm/joist-benchmarks/&quot;&gt;joist-benchmarks&lt;/a&gt; repo.&lt;/p&gt;&lt;p&gt;After running &lt;code dir=&quot;auto&quot;&gt;docker compose up -d&lt;/code&gt;, invoking &lt;code dir=&quot;auto&quot;&gt;yarn pipeline&lt;/code&gt; should run the benchmark.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;</content:encoded></item><item><title>Evolution of Defaults</title><link>https://joist-orm.io/blog/evolution-of-defaults/</link><guid isPermaLink="true">https://joist-orm.io/blog/evolution-of-defaults/</guid><pubDate>Thu, 27 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Joist’s mission is to model your application’s business logic, with first-class support for domain modeling features &amp;#x26; concepts.&lt;/p&gt;
&lt;p&gt;A great example of this is Joist’s support for something as simple as default values: for example, the &lt;code dir=&quot;auto&quot;&gt;Author.status&lt;/code&gt; field should default to &lt;code dir=&quot;auto&quot;&gt;Active&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Joist’s default values support grew from “the simplest thing possible” (requiring adhoc patterns that engineers would copy/paste around) to a robust, first-class feature (an explicit &lt;code dir=&quot;auto&quot;&gt;setDefault&lt;/code&gt; API that “just works”).&lt;/p&gt;
&lt;p&gt;This is a microcosm of Joist’s goal to identify the repeated patterns and pain points involved in “building a domain model”, and provide elegant features with a great DX.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;version-1-schema-defaults&quot;&gt;Version 1. Schema Defaults&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Joist’s initial defaults support was purposefully “as simple as possible”, and limited to &lt;code dir=&quot;auto&quot;&gt;DEFAULT&lt;/code&gt;s declared in the database schema, i.e. an &lt;code dir=&quot;auto&quot;&gt;is_archived&lt;/code&gt; field that defaults to &lt;code dir=&quot;auto&quot;&gt;FALSE&lt;/code&gt;, or a &lt;code dir=&quot;auto&quot;&gt;status_id&lt;/code&gt; that defaults to &lt;code dir=&quot;auto&quot;&gt;DRAFT&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TABLE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;example_table&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id &lt;/span&gt;&lt;span&gt;SERIAL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PRIMARY KEY&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;is_archived BOOL &lt;/span&gt;&lt;span&gt;DEFAULT&lt;/span&gt;&lt;span&gt; false,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;status_id &lt;/span&gt;&lt;span&gt;INTEGER&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DEFAULT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Joist’s codegen would recognize these, and “apply them immediately” when creating an entity:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;em&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt;(Author&lt;/span&gt;&lt;span&gt;, {}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(AuthorStatus&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Draft&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// already Draft&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isArchived&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// already false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This was super-simple, and had a few pros:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pro: The &lt;code dir=&quot;auto&quot;&gt;status&lt;/code&gt; is immediately within the &lt;code dir=&quot;auto&quot;&gt;em.create&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;I.e. you don’t have to wait for an &lt;code dir=&quot;auto&quot;&gt;em.flush&lt;/code&gt; to “see the database default”&lt;/li&gt;
&lt;li&gt;Any business logic can immediately start using the default&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Pro: No duplication of “draft is the default” between the database schema &amp;#x26; TypeScript code&lt;/li&gt;
&lt;li&gt;Con: Only supports static, hard-coded values
&lt;ul&gt;
&lt;li&gt;Ideally we’d like to write lambdas to calculate defaults, based on business logic&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;version-2-beforecreate-hooks&quot;&gt;Version 2. beforeCreate hooks&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Being limited to static &lt;code dir=&quot;auto&quot;&gt;DEFAULT&lt;/code&gt; values is not great, so the first way of implementing more complicated “dynamic defaults” was using Joist’s &lt;code dir=&quot;auto&quot;&gt;beforeCreate&lt;/code&gt; hooks:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;/** Any author created w/non-zero amount of books defaults to Published. */&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;authorConfig&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;beforeCreate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;books&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;undefined&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;books&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; AuthorStatus&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Published&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; AuthorStatus&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Draft&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; {  if (a.status === undefined) {    a.status = a.books.get.length &gt; 0 ? AuthorStatus.Published : AuthorStatus.Draft;  }})&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This was a quick-win b/c Joist already supported &lt;code dir=&quot;auto&quot;&gt;beforeCreate&lt;/code&gt; hooks, but had a few cons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pro: Supports arbitrary business logic
&lt;ul&gt;
&lt;li&gt;The load hint easily enables cross-entity calculations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Con: The default logic isn’t ran until &lt;code dir=&quot;auto&quot;&gt;em.flush&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Harder for business logic to rely on&lt;/li&gt;
&lt;li&gt;Creates inconsistency between “hard-coded defaults” (applied immediately in &lt;code dir=&quot;auto&quot;&gt;em.create&lt;/code&gt;) and “dynamic defaults” (applied during &lt;code dir=&quot;auto&quot;&gt;flush&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Con: Susceptible to hook ordering issues
&lt;ul&gt;
&lt;li&gt;If our default’s value depends on &lt;em&gt;other&lt;/em&gt; defaults, it is hard to ensure the other “runs first”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Con: Boilerplate/imperative (not really a first-class feature)
&lt;ul&gt;
&lt;li&gt;The code has to 1st check if &lt;code dir=&quot;auto&quot;&gt;a.status&lt;/code&gt; is already set (not a huge deal, but boilerplate)&lt;/li&gt;
&lt;li&gt;There is nothing in the code/API that identifies “this is a default”, instead we just have an adhoc pattern of “this is how our app sets defaults”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Con: Caused duplication with test factories
&lt;ul&gt;
&lt;li&gt;Our test factories often wanted “the same defaults” applied, but Joist’s factories are synchronous, which meant any logic that was “set in &lt;code dir=&quot;auto&quot;&gt;beforeCreate&lt;/code&gt;” wouldn’t be seen right away.&lt;/li&gt;
&lt;li&gt;To work around this, we often “wrote twice” default logic across our entities &amp;#x26; test factories—not great!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;version-3-adding-setdefault&quot;&gt;Version 3: Adding setDefault&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;We lived with the Version 1 &amp;#x26; 2 options for several years, because they were “good enough”, but for the 3rd version, we wanted to start “setting defaults” on the road to being “more than just good enough”.&lt;/p&gt;
&lt;p&gt;Specifically, we wanted a first-class, idiomatic way to “declaratively specify a field’s default value” instead of the previous “manually check the field in a &lt;code dir=&quot;auto&quot;&gt;beforeCreate&lt;/code&gt; hook”.&lt;/p&gt;
&lt;p&gt;So we added &lt;code dir=&quot;auto&quot;&gt;config.setDefault&lt;/code&gt;, which accepts the field name, it’s dependencies (if any), and a lambda that would calculate the default value:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;/** Calculate the Author.status default, based on number of books. */&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;authorConfig&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setDefault&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;books&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// a.books.get is available, but a.firstName is not, b/c it&apos;s not listed as a dependency&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;books&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; AuthorStatus&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Published&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; AuthorStatus&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Draft&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; {  // a.books.get is available, but a.firstName is not, b/c it&amp;#x27;s not listed as a dependency  return a.books.get.length &gt; 0 ? AuthorStatus.Published : AuthorStatus.Draft;})&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This was a great start, but we pushed it out knowingly half-baked:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pro: Provided scaffolding of a better future
&lt;ul&gt;
&lt;li&gt;Gave an idiomatic way to “declare defaults”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Pro: The type system enforces that the lambda only calls fields explicitly listed in the dependency param
&lt;ul&gt;
&lt;li&gt;This reused our &lt;code dir=&quot;auto&quot;&gt;ReactiveField&lt;/code&gt; infra and is great for ensuring dependencies aren’t missed&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Con: The dependencies weren’t actually used yet
&lt;ul&gt;
&lt;li&gt;“…ship early!”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Con: &lt;code dir=&quot;auto&quot;&gt;setDefault&lt;/code&gt; lambdas were still not invoked until &lt;code dir=&quot;auto&quot;&gt;em.flush&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;So we still had the “write defaults twice” problem with test factories&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;version-4-dependency-aware&quot;&gt;Version 4: Dependency Aware&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;After having the &lt;code dir=&quot;auto&quot;&gt;setDefault&lt;/code&gt; API in production for a few months, the next improvement was to capitalize on “knowing our dependencies” and allow defaults to depend on other defaults.&lt;/p&gt;
&lt;p&gt;For example, maybe our &lt;code dir=&quot;auto&quot;&gt;Author.status&lt;/code&gt; default needs to know whether any of the books are published (which itself is a default):&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// In `Author.ts`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;authorConfig&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setDefault&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, { books: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;anyBookPublished&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;books&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;some&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;/span&gt;&lt;span&gt; =&gt; &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt; === &lt;/span&gt;&lt;span&gt;BookStatus&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Published&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; anyBookPublished &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; AuthorStatus&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Published&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; AuthorStatus&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Draft&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// In `Book.ts`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;bookConfig&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setDefault&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;, {}, &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Some business logic that dynamically determines the status&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; BookStatus&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Published&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; {  const anyBookPublished = a.books.get.some(b =&gt; b.status === BookStatus.Published);  return anyBookPublished ? AuthorStatus.Published : AuthorStatus.Draft;})// In &amp;#x60;Book.ts&amp;#x60;bookConfig.setDefault(&amp;#x22;status&amp;#x22;, {}, b =&gt; {  // Some business logic that dynamically determines the status  return BookStatus.Published;});&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Now, if both a &lt;code dir=&quot;auto&quot;&gt;Book&lt;/code&gt; and an &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; are created at the same time, &lt;code dir=&quot;auto&quot;&gt;em.flush&lt;/code&gt; will ensure that the &lt;code dir=&quot;auto&quot;&gt;Book.status&lt;/code&gt; is calculated before invoking the &lt;code dir=&quot;auto&quot;&gt;Author.status&lt;/code&gt; default—&lt;em&gt;we’ve solved our ordering issue!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This was a major accomplishment—cross-entity defaults had been a thorn in our side for years.&lt;/p&gt;
&lt;p&gt;(Fwiw we readily admit this is a rare/obscure need—in our domain model of 100s of entities, we have only ~2-3 of these “cross-entity defaults”, so we want to be clear this is not necessarily a “must have” feature—but, when you need it, it’s extremely nice to have!)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pro: Finally unlocked cross-entity defaults&lt;/li&gt;
&lt;li&gt;Con: Still have the “write defaults twice” problem with factories&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;version-5-teaching-factories&quot;&gt;Version 5: Teaching Factories!&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The next DX iteration was solving the duplication of “factories want the defaults too!”.&lt;/p&gt;
&lt;p&gt;Looking more closely at this issue, Joist’s test factories are synchronous, which means we can create test data easily without any &lt;code dir=&quot;auto&quot;&gt;await&lt;/code&gt;s:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Given an author&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;newAuthor&lt;/span&gt;&lt;span&gt;(em);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// And a book&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;newBook&lt;/span&gt;&lt;span&gt;(em&lt;/span&gt;&lt;span&gt;, { author: &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// And setup something else using b.title&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// ...if there is &quot;default title logic&quot;, it will not have ran yet, which&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// can be confusing for tests/other logic expecting that behavior&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(b&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The lack of &lt;code dir=&quot;auto&quot;&gt;await&lt;/code&gt;s is very nice! But it does mean, if we really wanted &lt;code dir=&quot;auto&quot;&gt;b.title&lt;/code&gt; to &lt;em&gt;immediately&lt;/em&gt; reflect its production default, we had recode the default logic into the &lt;code dir=&quot;auto&quot;&gt;newBook&lt;/code&gt; factory:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;newBook&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;em&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;EntityManager&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DeepNew&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Book&lt;/span&gt;&lt;span&gt;&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;newTestInstance&lt;/span&gt;&lt;span&gt;(em&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; Book&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;title: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;recode the Book default logic here&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; {  return newTestInstance(em, Book, {    title: &amp;#x22;recode the Book default logic here&amp;#x22;,  });}&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;As before, for a while this was “good enough”—but finally in this iteration, we taught the factories to leverage their “each test’s data is already in memory” advantage and just invoke the defaults immediately during the &lt;code dir=&quot;auto&quot;&gt;newTestInstance&lt;/code&gt; calls.&lt;/p&gt;
&lt;p&gt;This works even for &lt;code dir=&quot;auto&quot;&gt;setDefault&lt;/code&gt;s that use load hints, like “author status depends on its books”:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// In `Author.ts`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;authorConfig&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setDefault&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, { books: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; }, &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;anyBookPublished&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;books&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;some&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;/span&gt;&lt;span&gt; =&gt; &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt; === &lt;/span&gt;&lt;span&gt;BookStatus&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Published&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; anyBookPublished &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; AuthorStatus&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Published&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; AuthorStatus&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Draft&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; {  const anyBookPublished = a.books.get.some(b =&gt; b.status === BookStatus.Published);  return anyBookPublished ? AuthorStatus.Published : AuthorStatus.Draft;})&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;In production, Joist can’t assume “the author’s books are already in-memory”, so &lt;code dir=&quot;auto&quot;&gt;em.flush&lt;/code&gt; would first load / &lt;code dir=&quot;auto&quot;&gt;await&lt;/code&gt; for the &lt;code dir=&quot;auto&quot;&gt;a.books&lt;/code&gt; to be loaded, and then invoke the lambda.&lt;/p&gt;
&lt;p&gt;However, because our tests know that &lt;code dir=&quot;auto&quot;&gt;a.books&lt;/code&gt; is already in memory, they can skip this &lt;code dir=&quot;auto&quot;&gt;await&lt;/code&gt;, and immediately invoke the lambda.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pro: We finally can remove the factory’s “write it twice” defaults&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;version-next-findorcreates&quot;&gt;Version Next: findOrCreates&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Always looking ahead, the next itch we have is that, currently, default lambdas that call async methods like &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;em.findOrCreate&lt;/code&gt; are still skipped during &lt;code dir=&quot;auto&quot;&gt;newTestInstance&lt;/code&gt; and only run during &lt;code dir=&quot;auto&quot;&gt;em.flush&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Which means, for these defaults, we still have remnants of the “write it twice” defaults anti-pattern—albeit very few of them!&lt;/p&gt;
&lt;p&gt;We should be able to lift this restriction as well, with a little bit of work (…maybe :thinking:, the &lt;code dir=&quot;auto&quot;&gt;newBook&lt;/code&gt; call is fundamentally synchronous, so maybe not).&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;slow-grind-to-perfection&quot;&gt;Slow Grind to Perfection&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Wrapping up, besides a “walk down memory lane”, the larger point of this post is highlighting Joist’s journey of continually grinding on DX polish—we’re about five years into &lt;a href=&quot;https://www.joelonsoftware.com/2001/07/21/good-software-takes-ten-years-get-used-to-it/&quot;&gt;Joel’s Good Software Takes 10 Years&lt;/a&gt;, so only another 5 to go! :smile:&lt;/p&gt;
&lt;p&gt;Of course, it’d be great for this evolution to happen more quickly—i.e. if we had a dependency-aware, factory-aware, amazing &lt;code dir=&quot;auto&quot;&gt;setDefault&lt;/code&gt; API from day one.&lt;/p&gt;
&lt;p&gt;But, often times jumping to an abstraction can be premature, and result in a rushed design—so sometimes it doesn’t hurt to “sit with the itch” for a little while, evolve it through multiple iterations of “good enough”, until finally a pleasant/robust solution emerges.&lt;/p&gt;
&lt;p&gt;And, perhaps most pragmatically, small iterations helps spread the implementation out over enough hack days that it can actually get shipped. :ship:&lt;/p&gt;</content:encoded></item><item><title>Recursive Relations</title><link>https://joist-orm.io/blog/recursive-relations/</link><guid isPermaLink="true">https://joist-orm.io/blog/recursive-relations/</guid><pubDate>Sat, 20 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Joist’s development is currently very incremental, and doesn’t have “big release” milestones &amp;#x26; release notes, but we recently released a notable new feature: &lt;a href=&quot;https://joist-orm.io/advanced/recursive-relations&quot;&gt;recursive relations&lt;/a&gt;. Check them out! :tada:&lt;/p&gt;</content:encoded></item><item><title>Is Joist the Best ORM, Ever?</title><link>https://joist-orm.io/blog/the-best-orm-ever/</link><guid isPermaLink="true">https://joist-orm.io/blog/the-best-orm-ever/</guid><pubDate>Sat, 11 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve been working on the Joist docs lately, specifically a &lt;a href=&quot;https://joist-orm.io/why-joist&quot;&gt;Why Joist?&lt;/a&gt; page, which ended up focusing more on “why Domain Models?” than a feature-by-feature description of Joist.&lt;/p&gt;
&lt;p&gt;Which is fine, but a good friend (and early Joist user) proofread it, and afterward challenged me that I was being too humble, and I should be more assertive about Joist being “THE BEST ORM FOR TYPESCRIPT AND POSTGRES” (his words), as he listed off his own personal highlights:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If it compiles, it works. “If you love TypeScript, you’ll love Joist.”&lt;/li&gt;
&lt;li&gt;It’s “really effing fast” (&lt;a href=&quot;https://joist-orm.io/goals/avoiding-n-plus-1s&quot;&gt;no N+1s&lt;/a&gt;, ever).&lt;/li&gt;
&lt;li&gt;We solve many common problems for you (&lt;a href=&quot;https://joist-orm.io/features/entity-manager#auto-batch-updates&quot;&gt;auto-batching updates&lt;/a&gt;, handling the insertion order of related entities, and have many patterns for &lt;a href=&quot;https://joist-orm.io/modeling/enum-tables&quot;&gt;enums&lt;/a&gt;, &lt;a href=&quot;https://joist-orm.io/modeling/relations#polymorphic-references&quot;&gt;polymorphic relations&lt;/a&gt;, etc.)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://joist-orm.io/testing/test-factories&quot;&gt;Factories&lt;/a&gt; make testing amazing.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All of these are true.&lt;/p&gt;
&lt;p&gt;But in thinking about his challenge, of pitching Joist specifically as “the best ORM for TypeScript &amp;#x26; Postgres”, I actually think I can be even more bullish and assert Joist is, currently, &lt;strong&gt;the best ORM, in any language, ever, TypeScript or otherwise&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Which is crazy, right? How could I possibly assert this?&lt;/p&gt;
&lt;p&gt;I have three reasons; admittedly the first two are not technically unique to Joist, but both foundational to its design and implementation, and the third that is one of Joist’s “special sauces”:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;JavaScript’s ability to solve N+1s via the event loop, and&lt;/li&gt;
&lt;li&gt;TypeScript’s ability to model loaded-ness in its type system.&lt;/li&gt;
&lt;li&gt;Joist’s “backend reactivity”&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;no-n1s-javascripts-event-loop&quot;&gt;No N+1s: JavaScript’s Event Loop&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;I’ve used many ORMs over the years, going back to Java’s Hibernate, Ruby’s ActiveRecord, and a few bespoke ones in between.&lt;/p&gt;
&lt;p&gt;Invariably, they all suffer from N+1s.&lt;/p&gt;
&lt;p&gt;I don’t want to repeat Joist’s existing &lt;a href=&quot;https://joist-orm.io/goals/avoiding-n-plus-1s&quot;&gt;Avoiding N+1s&lt;/a&gt; docs, but basically “entities are objects with fields/methods that incrementally lazy-load their relations from the database” is almost “too ergonomic”, and tempts programmers into using the abstraction when they shouldn’t (i.e. in a loop), at which point N+1s are inevitable.&lt;/p&gt;
&lt;p&gt;Again as described in “Avoiding N+1s”, JavaScript’s event loop forcing all I/O calls to “wait just a sec”, until the end of the event loop tick, gives Joist an amazing opportunity, of course via &lt;a href=&quot;https://github.com/graphql/dataloader&quot;&gt;dataloader&lt;/a&gt;, to de-dupe all the N+1s into a single SQL call.&lt;/p&gt;
&lt;p&gt;For everything.&lt;/p&gt;
&lt;p&gt;This works so well, that personally &lt;strong&gt;I don’t know that I ever want to work in a programming language/tech stack that cannot use this trick&lt;/strong&gt; (at least to build backend/line-of-business applications).&lt;/p&gt;
&lt;p&gt;Granted, JavaScript is not the only language with an event loop—async Rust is a thing, Python has asyncio, and even &lt;a href=&quot;https://vertx.io/&quot;&gt;Vert.x&lt;/a&gt; on the JVM provides it (I prototyped “dataloader ported to Vert.x” several years ago), and either Rust or the JVM (Scala!) would be pretty tempting just in terms of “faster than JavaScript” performance.&lt;/p&gt;
&lt;p&gt;But the event loop is only part of the story—another critical part is TypeScript’s type system.&lt;/p&gt;
&lt;div&gt;&lt;p&gt;Other TypeScript ORMs like Prisma &amp;#x26; Drizzle “solve” N+1s by just not modeling your domain as entities (with lazy-loaded relations), and instead force/assume a single/large up-front query that returns an immutable tree of POJOs.&lt;/p&gt;&lt;p&gt;This does remove the most obvious N+1 footgun (lazy-loaded relations), but it also fundamentally restricts your ability to decompose business logic into smaller/reusable methods, because now any logic that touches the database must be done “in bulk” directly by your code, and often crafted in SQL specifically to how each individual endpoint is accessing the data.&lt;/p&gt;&lt;p&gt;(Concretely, if you had a &lt;code dir=&quot;auto&quot;&gt;saveAuthor&lt;/code&gt; endpoint with logic/queries to validate “this author is valid”, and now write a batch &lt;code dir=&quot;auto&quot;&gt;saveAuthors&lt;/code&gt; endpoint, you could not reuse the “written for one entity” logic without rewriting it to work at the new endpoint’s grouped/batch level of granularity. Or similar for &lt;code dir=&quot;auto&quot;&gt;saveBook&lt;/code&gt; logic that you want to use within a &lt;code dir=&quot;auto&quot;&gt;saveAuthor&lt;/code&gt; that also upserts multiple children books.)&lt;/p&gt;&lt;p&gt;Instead, Joist’s auto-batching lets you ergonomically write code at the individual entity abstraction level (whether in a loop, or in per-entity validation rules or lifecycle hooks), but still get performant-by-default batched queries.&lt;/p&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;loaded-subgraphs-typescripts-type-system&quot;&gt;Loaded Subgraphs: TypeScript’s Type System&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;After solving N+1s with the event loop, the next biggest ergonomic problem in traditional, entity-based ORMs is tracking (or basically not tracking) loaded-ness in the type system.&lt;/p&gt;
&lt;p&gt;Because you can’t have your entire relational database in memory, domain models must incrementally load their data from the database, as your business logic’s codepaths decide which parts they need to read.&lt;/p&gt;
&lt;p&gt;This was another downfall of the Hibernate/ActiveRecord ORMs: there was no notion of “is this relation loaded yet?”, and so any random relation access could trigger the surprise of an expensive database I/O call, as that relation was lazy-loaded from the database.&lt;/p&gt;
&lt;p&gt;Joist solves this by &lt;a href=&quot;https://joist-orm.io/goals/load-safe-relations&quot;&gt;statically typing all relations&lt;/a&gt; as “unloaded” by default, i.e. accessing an Author’s books requires calling &lt;code dir=&quot;auto&quot;&gt;a1.books.load()&lt;/code&gt;, which returns a &lt;code dir=&quot;auto&quot;&gt;Promise&lt;/code&gt; (which is also key to the N+1 prevention above).&lt;/p&gt;
&lt;p&gt;Which is great, I/O calls are now obvious, but “do an &lt;code dir=&quot;auto&quot;&gt;await&lt;/code&gt; for every relation access” would really suck (we tried that), so Joist goes further and uses TypeScript’s type system to not only track individual relation loaded-ness (like &lt;code dir=&quot;auto&quot;&gt;author1.books&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;book2.authors&lt;/code&gt;), but mark &lt;strong&gt;entire subgraphs&lt;/strong&gt; of entities as populated/loaded relations and hence synchronously accessible:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Load the Author plus the specific books + reviews subgrpah&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;a1&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;em&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;load&lt;/span&gt;&lt;span&gt;(Author&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;a:1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;populate: { books: { reviews: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;comments&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; } },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// a1 is typed as Loaded&amp;#x3C;Author, { books: { reviews: &quot;comments&quot; } }&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Tada, no more await Promise.all&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;a1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;books&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;forEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;book&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;book&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reviews&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;forEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;review&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(review&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;comments&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt;// Tada, no more await Promise.alla1.books.get.forEach((book) =&gt; {  book.reviews.get.forEach((review) =&gt; {    console.log(review.comments.get.length);  });})&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This combination of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Explicit &lt;code dir=&quot;auto&quot;&gt;.load()&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;await&lt;/code&gt; calls for any I/O, but leveraging&lt;/li&gt;
&lt;li&gt;Mapped types to allow compiler-checked &lt;strong&gt;synchronous&lt;/strong&gt; access&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For me, is also something that &lt;strong&gt;I never want to work without again&lt;/strong&gt;. It’s just so nice.&lt;/p&gt;
&lt;p&gt;Unlike JavaScript not having a monopoly on the event loop, for these mapped types I believe TypeScript effectively does have a lock on this capability, from a programming language/type system perspective.&lt;/p&gt;
&lt;p&gt;Creating “new types” in other programming languages is generally handled by macros (Scala and Rust), or I suppose Haskell’s higher-kinded-types. But, as far as I know, none of them can combine TypeScript “mapped type + conditional type” features in a way that would allow this “take my user-defined type (Author)” and “this user-defined populate hint type” and fuse them together into a new type, that is “the author with this specific subgraph of fields marked as loaded”.&lt;/p&gt;
&lt;p&gt;I’m happy to be corrected on this, but I think TypeScript is the only mainstream programming language that can really power Joist’s &lt;code dir=&quot;auto&quot;&gt;Loaded&amp;#x3C;Author, { books: &quot;reviews&quot; }&gt;&lt;/code&gt;-style adhoc typing of subgraphs, or at least this easily.&lt;/p&gt;
&lt;div&gt;&lt;p&gt;Other TypeScript ORMs (Prisma, Drizzle, Kysley, etc.) also leverage TypeScript’s mapped types to create dynamic shapes of data, which is legitimately great.&lt;/p&gt;&lt;p&gt;However, they all have the fundamental approach of issuing “one-shot” queries that return immutable trees of POJOs, directly mapped from your SQL tables, and not subgraphs of entities that can have non-SQL abstractions &amp;#x26; be further incrementally loaded as/if needed (see &lt;a href=&quot;https://joist-orm.io/why-joist&quot;&gt;Why Joist&lt;/a&gt; for more on this).&lt;/p&gt;&lt;p&gt;You can generally see, for both issues covered so far (N+1s and statically-typed loaded-ness), most TypeScript ORMs have “solved” these issues by just removing the features all together, and restricting themselves to be “sophisticated query builders”.&lt;/p&gt;&lt;p&gt;Joist’s innovation is keeping the entity-based, incremental-loading mental model that is historically very popular/idiomatic for ORMs (particularly Ruby’s ActiveRecord), and just fundamentally fixing it to not suck.&lt;/p&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;joists-backend-reactivity&quot;&gt;Joist’s Backend Reactivity&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This 3rd section is the first feature that is unique to Joist itself: Joist’s “backend reactivity”.&lt;/p&gt;
&lt;p&gt;Many ORMs have lifecycle hooks (this entity was created, updated, or deleted—which Joist &lt;a href=&quot;https://joist-orm.io/modeling/lifecycle-hooks&quot;&gt;does as well&lt;/a&gt;), to organize side effects/business logic of “when X changes, do Y”.&lt;/p&gt;
&lt;p&gt;But just lifecycle hooks by themselves can become tangled, complicated, and a well-known morass of complexity and “spooky action at a distance”.&lt;/p&gt;
&lt;p&gt;This is because they’re basically “Web 1.0” imperative spaghetti code, where you have to manually instrument each mutation that might trigger a side effect.&lt;/p&gt;
&lt;p&gt;(Concretely, lets say you have a rule that needs to look at both an author and its books. With raw lifecycle hooks, you must separately instrument both the “author update” and “book update” hooks to call your “make sure this author + books combination is still valid” logic. This can become tedious and error-prone, to get all the right hooks instrumented.)&lt;/p&gt;
&lt;p&gt;Instead, Joist’s &lt;a href=&quot;https://joist-orm.io/modeling/reactive-fields&quot;&gt;reactive fields&lt;/a&gt; and &lt;a href=&quot;https://joist-orm.io/modeling/validation-rules&quot;&gt;reactive validation rules&lt;/a&gt;  take the lessons of “declarative reactivity” from the Mobx/Solid/reactivity-aware frontend world, and bring it to the backend: reactive rules &amp;#x26; fields declare in one place what their “upstream dependencies” are, and Joist just handles wiring up the necessary cross-entity reactivity.&lt;/p&gt;
&lt;p&gt;This brings a level of ease, specificity, and rigor to what are still effectively lifecycle hooks under the hood, that really makes them pleasant to work with.&lt;/p&gt;
&lt;div&gt;&lt;p&gt;The declarative nature of Joist’s domain model-wide reactivity graph is also very amenable to DX tooling &amp;#x26; documentation generation, but we’ve not yet deeply explored/delivered any functionality that leverages it.&lt;/p&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;conclusion-best-orm-ever&quot;&gt;Conclusion: Best ORM Ever?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;So, these three features are what back up my exaggerated “best ORM ever” assertion.&lt;/p&gt;
&lt;p&gt;If tomorrow, I suddenly could not use Joist, and had to find another ORM to use (or, in general, build any sort of application backend on top of a relational database), in any current/mainstream programming language, without a doubt I would want:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Bullet-proof N+1 prevention,&lt;/li&gt;
&lt;li&gt;Tracking loaded relation/subgraph state in the type system, and&lt;/li&gt;
&lt;li&gt;Backend reactivity, for declarative cross-entity validation rules and reactive fields.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And Joist is the only ORM that does all three of these: two of which are uniquely enabled by the JavaScript/TypeScript stack, and the third just part of Joist’s own innovation.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;disclaimer-1-uncomfortably-bold-claims&quot;&gt;Disclaimer 1: Uncomfortably Bold Claims&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;I usually don’t like making bold/absolutist claims, like “this or that framework is ‘the best’” or “technology x/y/z is terrible” or what not.&lt;/p&gt;
&lt;p&gt;I did enough of that early in my career, and at this point I’m more interested in “what are the trade-offs?” and “what’s the best tool for this specific use case?”&lt;/p&gt;
&lt;p&gt;So, I hold two somewhat incongruent thoughts in my head, as I am both:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Very confident that Joist is “the best” way to build application backends on top of a relational database, for a large majority of use cases/teams/codebases, but I also&lt;/li&gt;
&lt;li&gt;Recognize it’s “framework” / entity approach (see &lt;a href=&quot;https://joist-orm.io/why-joist&quot;&gt;Why Joist&lt;/a&gt;) might be either too opinionated or too much abstraction for some people’s tastes, and just in general choices &amp;#x26; alternatives are always great to have.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My guess is if you tried Joist, you would quickly come to like it, but it’s also perfectly fine if not!&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;disclaimer-2-still-a-lot-to-do&quot;&gt;Disclaimer 2: Still a Lot To Do&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Similar to the two incongruent thoughts above, another two semi-contradictory thoughts is the disclaimer that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Joist’s core is very solid and vetted by 4+ years of production usage &amp;#x26; continual iteration at &lt;a href=&quot;https://www.homebound.com/&quot;&gt;Homebound&lt;/a&gt;, but also&lt;/li&gt;
&lt;li&gt;There’s still a lot of work to do, obviously supporting other databases, but also the myriad fun, incremental improvement ideas we’re tracking in the issue tracker, and of course even more that we’ve not thought of yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;feedback&quot;&gt;Feedback&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you have thoughts, questions, or feedback, please let us know! Feel free to join the &lt;a href=&quot;https://discord.gg/ky9VTQugqu&quot;&gt;Joist discord&lt;/a&gt;, or file issues on the GitHub repo if you try Joist and run into any issues.&lt;/p&gt;
&lt;p&gt;Despite all the hubris in this post, we are still a very small project &amp;#x26; community, and so have a lot of growth and improvement ahead of us.&lt;/p&gt;
&lt;p&gt;Thanks for the read!&lt;/p&gt;</content:encoded></item><item><title>New NextJS Sample App</title><link>https://joist-orm.io/blog/nextjs-sample-app/</link><guid isPermaLink="true">https://joist-orm.io/blog/nextjs-sample-app/</guid><pubDate>Sun, 21 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;We’ve added a new &lt;a href=&quot;https://github.com/joist-orm/joist-nextjs-sample/&quot;&gt;NextJS + Joist&lt;/a&gt; sample app that shows how Joist can be used in a NextJS application, with several benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automatic N+1 Prevention&lt;/li&gt;
&lt;li&gt;JSON Payload/Props Creation&lt;/li&gt;
&lt;li&gt;Optional Join-based Preloading&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This post gives a short overview; if you’d like to watch a video, we also have a &lt;a href=&quot;https://youtu.be/H_qJdKUS9D0&quot;&gt;YouTube video&lt;/a&gt; that walks through the sample app.&lt;/p&gt;
&lt;div display:=&quot;&quot; x27=&quot;&quot; grid=&quot;&quot; placeitems:=&quot;&quot; center=&quot;&quot;&gt;
  &lt;iframe width=&quot;750&quot; height=&quot;420&quot; src=&quot;https://www.youtube.com/embed/H_qJdKUS9D0?si=qUiRr0GTMrQCgayC&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;two-render-tree-approaches&quot;&gt;Two Render Tree Approaches&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;While building the sample app, we found two fundamental ways of structuring a NextJS app’s render tree:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Fewer RSCs (left side), that prop drill data to the Client Components
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;table.tsx&lt;/code&gt; is a server component that loads all data for the tree&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;author-rcc-card.tsx&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;book-rcc-preview.tsx&lt;/code&gt; are client components that accept prop-drilled data&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Mostly RSCs (right side), with Client Components only at the bottom
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;table.tsx&lt;/code&gt; is a server component but only loads what it needs&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;author-rsc-card.tsx&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;book-rsc-preview.tsx&lt;/code&gt; are RSC and do their own data loading&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div padding:=&quot;&quot; x27=&quot;&quot; 24px=&quot;&quot;&gt;
  &lt;img src=&quot;https://joist-orm.io/images/nextjs-sample-single-multiple-rscs.png&quot;&gt;
&lt;/div&gt;
&lt;p&gt;The top-level &lt;code dir=&quot;auto&quot;&gt;Table&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;table.tsx&lt;/code&gt; component renders each of these side-by-side, so we can see the differences, and observe some pros/cons of each approach.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;With mostly RSC components, it’s easy to decompose data loading away from the top-level component.&lt;/p&gt;
&lt;p&gt;For example, the &lt;code dir=&quot;auto&quot;&gt;AuthorRscCard&lt;/code&gt; can make its own data loading calls, and even if it’s render many pages on the page, Joist will de-dupe across the &lt;code dir=&quot;auto&quot;&gt;N&lt;/code&gt; sibling &lt;code dir=&quot;auto&quot;&gt;AuthorRscCard&lt;/code&gt;s, and batch into a single SQL call.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; AuthorCardProps &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;/** RSCs can accept the domain model enities as a prop. */&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;author&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Author&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;addBook&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt;&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;/** The RSC version of AuthorCard can load it&apos;s own data. */&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AuthorRscCard&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;author&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;addBook&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;AuthorCardProps&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// This will be auto-batched if many cards render at once&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;books&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;author&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;books&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;load&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// Or if you wanted a tree of data, this will also be auto-batched&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;loaded&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;author&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;populate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ books: { reviews: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;ratings&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; } }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;...jsx&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; Promise&lt;void&gt;;};/** The RSC version of AuthorCard can load it&amp;#x27;s own data. */export async function AuthorRscCard({ author, addBook }: AuthorCardProps) {  // This will be auto-batched if many cards render at once  const books = await author.books.load();  // Or if you wanted a tree of data, this will also be auto-batched  const loaded = await author.populate({ books: { reviews: &amp;#x22;ratings&amp;#x22; } });  return &lt;div&gt;...jsx&lt;/div&gt;;}&quot;&gt;&lt;div&gt;&lt;/div&gt;
&lt;p&gt;This is nice because it allows the &lt;code dir=&quot;auto&quot;&gt;AuthorRscCard&lt;/code&gt; to be more self-sufficient, and allow the parent table component to be unaware of its children loading details.&lt;/p&gt;

&lt;li&gt;
&lt;p&gt;With mostly Client components, the opposite happens, and only the parent can make database / &lt;code dir=&quot;auto&quot;&gt;EntityManager&lt;/code&gt; calls, and so is responsible for loading all the data for its children, and passing it as JSON via props:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; AuthorCardProps &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;/** RCCs must accept a POJO of `Author` + all nested data. */&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;author&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AuthorPayload&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;addBook&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt;&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;/** The RCC version of AuthorCard accepts the `AuthorPayload`. */&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AuthorRccCard&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;author&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;addBook&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;AuthorCardProps&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// can only use data already available on `author`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; Promise&lt;void&gt;;};/** The RCC version of AuthorCard accepts the &amp;#x60;AuthorPayload&amp;#x60;. */export function AuthorRccCard({ author, addBook }: AuthorCardProps) {  // can only use data already available on &amp;#x60;author&amp;#x60;}&quot;&gt;&lt;div&gt;&lt;/div&gt;
&lt;p&gt;Even though the up-front data load can become awkward, it does give more opportunities for optimizations; for example Joist can use join-based preloading to load a single tree of &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;Book&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;Review&lt;/code&gt; entities in a single SQL call, which is even better optimization than the “one query per layer” N+1 prevention of the RSC-based approach.&lt;/p&gt;


&lt;div&gt;&lt;h2 id=&quot;automatic-n1-prevention&quot;&gt;Automatic N+1 Prevention&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;In either approach, Joist’s N+1 prevention auto-batches database calls, even if they are made across separate component renders.&lt;/p&gt;
&lt;p&gt;I.e. in the RSC components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The top-level &lt;code dir=&quot;auto&quot;&gt;Table&lt;/code&gt; component makes 1 SQL call for all &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; entities.&lt;/li&gt;
&lt;li&gt;All 2nd-level &lt;code dir=&quot;auto&quot;&gt;AuthorRscCard&lt;/code&gt; cards each make their own &lt;code dir=&quot;auto&quot;&gt;author.books.load()&lt;/code&gt; (or &lt;code dir=&quot;auto&quot;&gt;author.populate(...)&lt;/code&gt;) call, but because they’re all rendered in the same event loop, Joist batches all the &lt;code dir=&quot;auto&quot;&gt;load&lt;/code&gt; calls into 1 SQL call&lt;/li&gt;
&lt;li&gt;Any 3rd-level components would have their &lt;code dir=&quot;auto&quot;&gt;load&lt;/code&gt; calls batched as well.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the React Client Component approach, this auto-batching is admittedly not as necessary, assuming a singular top-level component, like &lt;code dir=&quot;auto&quot;&gt;Table&lt;/code&gt;, loads all the data at once anyway (although, as mentioned later, Joist can optimize that as well).&lt;/p&gt;
&lt;p&gt;See the &lt;a href=&quot;https://joist-orm.io/goals/avoiding-n-plus-1s&quot;&gt;Avoiding N+1s&lt;/a&gt; section of our docs for more information.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;json-payloadprops-creation&quot;&gt;JSON Payload/Props Creation&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Since the client components cannot make their own async data calls, the top-level &lt;code dir=&quot;auto&quot;&gt;Table&lt;/code&gt; components is responsible for loading all the data into a JSON payload, and passing it down to the children as props.&lt;/p&gt;
&lt;p&gt;Joist entities have an easy way of doing this is, via a &lt;code dir=&quot;auto&quot;&gt;toJSON&lt;/code&gt; method that takes the shape of data to create:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Define the shape of data to create&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;authorHint&lt;/span&gt;&lt;span&gt; = {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;firstName: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;books: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;title: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;reviews:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;rating&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;customField&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; + &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} satisfies &lt;/span&gt;&lt;span&gt;JsonHint&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Author&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// This typedef can be used in the client-side props, or to match any&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// endpoint-based respones types like for REST/OpenAPI.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; AuthorPayload &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JsonPayload&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Author&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt; authorHint&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;payload&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toJSON&lt;/span&gt;&lt;span&gt;(authorHint);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;button title=&quot;Copy to clipboard&quot; data-copied=&quot;Copied!&quot; data-code=&quot;&quot;&gt; a.id + a.title,} satisfies JsonHint&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/void&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/li&gt;&lt;/void&gt;&lt;/button&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/li&gt;&lt;/ul&gt;</content:encoded></item></channel></rss>