<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:fh="http://purl.org/syndication/history/1.0"><channel><title>Joist | Blog</title><description/><link>https://joist-orm.io/</link><language>en</language><fh:complete/><atom:link rel="self" href="https://joist-orm.io/blog/rss.xml"/><item><title>Joist 2.3 Released with Rails-style Scopes!</title><link>https://joist-orm.io/blog/joist-2-3/</link><guid isPermaLink="true">https://joist-orm.io/blog/joist-2-3/</guid><description>Joist 2.3 is out, headlined by Rails-style scopes, batched paginated finds, enum collections, native Node 26 Temporal support, smarter batched queries, and a large round of hot-path performance work.</description><pubDate>Sat, 08 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Joist &lt;a href=&quot;https://github.com/joist-orm/joist-orm/commit/108b79314224bca1a75ef5bc6fc8071f8f6c18b6&quot;&gt;2.3&lt;/a&gt; is out! (…as of a month ago, we’re behind on the blog post! 😅)&lt;/p&gt;
&lt;p&gt;The release highlights are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rails-style &lt;code dir=&quot;auto&quot;&gt;scope&lt;/code&gt;s for naming and composing reusable &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; filters&lt;/li&gt;
&lt;li&gt;Scaffolding for GraphQL query resolvers&lt;/li&gt;
&lt;li&gt;Paginated &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; calls are now auto-batched&lt;/li&gt;
&lt;li&gt;New &lt;code dir=&quot;auto&quot;&gt;EnumCollection&lt;/code&gt;s for many-to-many join tables that point at an enum&lt;/li&gt;
&lt;li&gt;Native &lt;code dir=&quot;auto&quot;&gt;Temporal&lt;/code&gt; support on Node 26, with no polyfill required&lt;/li&gt;
&lt;li&gt;Smarter batched queries via constant inlining and boolean pruning&lt;/li&gt;
&lt;li&gt;A large round of hot-path performance work, with 20-50%+ speedups across flush, hydration, and &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; micro-benchmarks&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;rails-style-scopes&quot;&gt;Rails-Style Scopes&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The headline 2.3 feature is &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1892&quot;&gt;#1892&lt;/a&gt;: Rails-style scopes 🎉, which let you name and compose common &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; filters directly on your entity classes.&lt;/p&gt;
&lt;p&gt;You declare scopes as static fields, using the &lt;code dir=&quot;auto&quot;&gt;scope&lt;/code&gt; function:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;src/entities/Author.ts&lt;/span&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;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;static&lt;/span&gt;&lt;span&gt; adult &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;scope&lt;/span&gt;&lt;span&gt;({ age: { gte: &lt;/span&gt;&lt;span&gt;18&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;static&lt;/span&gt;&lt;span&gt; active &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;scope&lt;/span&gt;&lt;span&gt;({ deletedAt: &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;static&lt;/span&gt;&lt;span&gt; popular &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;scope&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; a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isPopular&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&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;static&lt;/span&gt;&lt;span&gt; hasBooks &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;scope&lt;/span&gt;&lt;span&gt;({ books: &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;static&lt;/span&gt;&lt;span&gt; booksReviewedBy &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; scope&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;reviewer&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;=&gt;&lt;/span&gt;&lt;span&gt; ({ books: { reviewer } }));&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;And then reuse and chain them throughout your codebase:&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;await&lt;/span&gt;&lt;span&gt; Author&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;adult&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;find&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;await&lt;/span&gt;&lt;span&gt; Author&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;adult&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;popular&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;find&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;await&lt;/span&gt;&lt;span&gt; Author&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;active&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;where&lt;/span&gt;&lt;span&gt;({ firstName: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;a1&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;find&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;await&lt;/span&gt;&lt;span&gt; Author&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;booksReviewedBy&lt;/span&gt;&lt;span&gt;(reviewer)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt;(em);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Scopes are syntax-sugar for Joist’s regular &lt;a href=&quot;https://joist-orm.io/features/queries-find&quot;&gt;em.find&lt;/a&gt;, so they share the same filter syntax and semantics, including relation filters across one-to-many, many-to-one, and many-to-many paths.&lt;/p&gt;
&lt;p&gt;A few things scopes can do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Chain with &lt;code dir=&quot;auto&quot;&gt;AND&lt;/code&gt; semantics&lt;/strong&gt;, i.e. &lt;code dir=&quot;auto&quot;&gt;Author.adult.popular&lt;/code&gt; finds authors that match both the &lt;code dir=&quot;auto&quot;&gt;adult&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;popular&lt;/code&gt; scopes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Take parameters&lt;/strong&gt; via &lt;code dir=&quot;auto&quot;&gt;scope.fn&lt;/code&gt;, i.e. &lt;code dir=&quot;auto&quot;&gt;Author.named(&quot;a&quot;)&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;Author.taggedWith(&quot;fiction&quot;)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add additional criteria&lt;/strong&gt; at each call site, i.e. &lt;code dir=&quot;auto&quot;&gt;Author.adult.orderBy({ createdAt: &quot;DESC&quot; }).limit(10)&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;.softDeletes(&quot;include&quot;)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drop into &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; anywhere a filter is expected&lt;/strong&gt;, i.e. &lt;code dir=&quot;auto&quot;&gt;em.find(Author, Author.adult)&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;em.find(Book, { author: Author.adult })&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composition&lt;/strong&gt; You can defined new scopes by combining others: &lt;code dir=&quot;auto&quot;&gt;static popularAdult = Author.popular.adult&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Similar to regular &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; queries, scopes also support &lt;code dir=&quot;auto&quot;&gt;alias&lt;/code&gt; for complex predicates (i.e. &lt;code dir=&quot;auto&quot;&gt;AND&lt;/code&gt;s and &lt;code dir=&quot;auto&quot;&gt;OR&lt;/code&gt;s) that a plain inline filter can’t express, like a top-level &lt;code dir=&quot;auto&quot;&gt;OR&lt;/code&gt; that spans two different joined tables:&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;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;// Authors with a book that is either titled &quot;b1&quot; or has a 3-star review.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; titleOrRated &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;scope&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;const&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;r&lt;/span&gt;&lt;span&gt;] = &lt;/span&gt;&lt;span&gt;aliases&lt;/span&gt;&lt;span&gt;(Book&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;BookReview);&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;/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;where: { books: { as: b, reviews: { as: r } } },&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;conditions: { or: [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;eq&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;b1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;), r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;rating&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&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;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;/figure&gt;&lt;/div&gt;
&lt;p&gt;Joist’s scopes are heavily inspired by Rails scopes 🙏, but are strongly-typed and adapted to fit Joist’s conventions–most notably, scopes always require a trailing &lt;code dir=&quot;auto&quot;&gt;.find(em)&lt;/code&gt; (or &lt;code dir=&quot;auto&quot;&gt;findOne&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;findOneOrFail&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;findCount&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;findIds&lt;/code&gt;) so Joist knows which &lt;code dir=&quot;auto&quot;&gt;EntityManager&lt;/code&gt; to use for loading and identity caching.&lt;/p&gt;
&lt;aside aria-label=&quot;Caution&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Caution&lt;/p&gt;&lt;div&gt;&lt;p&gt;To achieve the chained/recursive &lt;code dir=&quot;auto&quot;&gt;Author.adult.active&lt;/code&gt; static typing, Joist generates an &lt;code dir=&quot;auto&quot;&gt;AuthorScopes&lt;/code&gt; interface in each &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;Entity&gt;Codegen.ts&lt;/code&gt; file with a key for each declared &lt;code dir=&quot;auto&quot;&gt;scope&lt;/code&gt; field, so you should re-run &lt;code dir=&quot;auto&quot;&gt;joist-codegen&lt;/code&gt; after each change to your &lt;code dir=&quot;auto&quot;&gt;scope&lt;/code&gt; declarations.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;See the new &lt;a href=&quot;https://joist-orm.io/features/scope-queries&quot;&gt;Scope Queries&lt;/a&gt; docs for the full feature set.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;graphql-query-scaffolding&quot;&gt;GraphQL Query Scaffolding&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Joist has had scaffolding for GraphQL object resolvers &amp;#x26; save mutations for awhile now, but we finally got around to adding query scaffolding in &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1865&quot;&gt;#1865&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This means that after adding your &lt;code dir=&quot;auto&quot;&gt;authors&lt;/code&gt; database table, Joist will (if you’ve enabled the GraphQL scaffolding) codegen not only an &lt;code dir=&quot;auto&quot;&gt;Author.ts&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;AuthorCodegen.ts&lt;/code&gt; file, but also will auto-create a &lt;code dir=&quot;auto&quot;&gt;schema/Author.graphql&lt;/code&gt; file with a basic &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; type and a &lt;code dir=&quot;auto&quot;&gt;Query&lt;/code&gt; extension for fetching authors:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;schema/Author.graphql&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Query&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; Newly added in this 2.3 release &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;author&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;ID&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;&quot; Newly added in this 2.3 release &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;authors&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;/span&gt;&lt;span&gt;AuthorFilter&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;limit&lt;/span&gt;&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;&lt;span&gt;offset&lt;/span&gt;&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;AuthorsPage&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;extend&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Mutation&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; Already scaffolded in earlier releases. &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;saveAuthor&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;input&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SaveAuthorInput&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;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;As well as default implementations &lt;code dir=&quot;auto&quot;&gt;authorQuery.ts&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;authorsQuery.ts&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;src/resolvers/author/authorQuery.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &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;Pick&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;QueryResolvers&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;author&lt;/span&gt;&lt;span&gt;&quot;&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;&lt;/span&gt;&lt;span&gt;async &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;args&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ctx&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;return &lt;/span&gt;&lt;span&gt;ctx&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;args&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;/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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;src/resolvers/author/authorsQuery.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export const &lt;/span&gt;&lt;span&gt;authors&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Pick&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;QueryResolvers&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;authors&lt;/span&gt;&lt;span&gt;&quot;&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;&lt;/span&gt;&lt;span&gt;async &lt;/span&gt;&lt;span&gt;authors&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;_&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;ctx&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;return &lt;/span&gt;&lt;span&gt;paginate&lt;/span&gt;&lt;span&gt;(ctx&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;args)&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;As always, if you need to change any of the scaffolded output, just update it!&lt;/p&gt;
&lt;p&gt;Joist will never overwrite your changes.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;batched-paginated-finds&quot;&gt;Batched Paginated Finds&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Joist’s &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; has always auto-batched its &lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt; queries (part of our N+1 prevention), but previously &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; did not support batching &lt;em&gt;paginated&lt;/em&gt; queries, i.e. queries wanting to use &lt;code dir=&quot;auto&quot;&gt;OFFSET&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;LIMIT&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This limitation was not an issue for top-level endpoints doing pagination, like &lt;code dir=&quot;auto&quot;&gt;GET /authors?offset=100&amp;#x26;limit=50&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;query / authors(offset: 100)&lt;/code&gt;, but it does become an issue if you want to return “only the first &lt;code dir=&quot;auto&quot;&gt;N&lt;/code&gt; grandchildren” in a nested child, like this GraphQL 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;query&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;authors&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&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;&quot;&lt;/span&gt;&lt;span&gt;active&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;books&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; The `reviews` field will invoked once per author, per book &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;reviews&lt;/span&gt;&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;&lt;span&gt;5&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;rating&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;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This &lt;code dir=&quot;auto&quot;&gt;reviews(first: 5)&lt;/code&gt; is basically “a paginated &lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt; in a loop”, which now in &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1836&quot;&gt;#1836&lt;/a&gt; will also be batched into a single SQL query for all children invoking the query.&lt;/p&gt;
&lt;p&gt;Because &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; now supports &lt;code dir=&quot;auto&quot;&gt;limit&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;offset&lt;/code&gt;, we don’t have a need for legacy &lt;code dir=&quot;auto&quot;&gt;em.findPaginated&lt;/code&gt; method, which was the previous way to do (unbatched) paginated finds, anymore, so it’s been removed.&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;&lt;code dir=&quot;auto&quot;&gt;joist-codegen&lt;/code&gt; will automatically codemod any &lt;code dir=&quot;auto&quot;&gt;em.findPaginated&lt;/code&gt; calls with &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt;.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;Now any &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; can accept &lt;code dir=&quot;auto&quot;&gt;limit&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;offset&lt;/code&gt; and still get Joist’s auto-batching:&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;aReviews&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;bReviews&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;&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;(BookReview, { book: b1 }, { limit: &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;, orderBy: { id: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;ASC&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;em&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt;(BookReview, { book: b2 }, { limit: &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;, orderBy: { id: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;ASC&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;We covered this in detail in its own &lt;a href=&quot;https://joist-orm.io/blog/batched-paginated-finds&quot;&gt;Batched Paginated Finds&lt;/a&gt; post.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;enum-collections&quot;&gt;Enum Collections&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Joist has historically supported modeling “lists of enums” via &lt;code dir=&quot;auto&quot;&gt;int[]&lt;/code&gt; enum-array columns, but &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1908&quot;&gt;#1908&lt;/a&gt; adds a second option: a true many-to-many join table between an entity and an enum, modeled as an &lt;code dir=&quot;auto&quot;&gt;EnumCollection&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example, to give &lt;code dir=&quot;auto&quot;&gt;Publisher&lt;/code&gt; a list of “logo colors”, where &lt;code dir=&quot;auto&quot;&gt;Color&lt;/code&gt; is an enum in your domain model, create a join table between &lt;code dir=&quot;auto&quot;&gt;publishers&lt;/code&gt; and the &lt;code dir=&quot;auto&quot;&gt;color&lt;/code&gt; enum table:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;src/migrations/2026-06-29-add-publisher-logo-colors.ts&lt;/span&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;up&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;&lt;span&gt;&lt;span&gt;MigrationBuilder&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;createManyToManyTable&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;&quot;&lt;/span&gt;&lt;span&gt;publisher_logo_colors&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;&quot;&lt;/span&gt;&lt;span&gt;publishers&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; {table: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; column: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;logo_color_id&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;Joist recognizes that one side points at an enum table and generates an &lt;code dir=&quot;auto&quot;&gt;EnumCollection&lt;/code&gt; on the entity side (only the entity side; the enum does not get a reverse relation):&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;src/entities/PublisherCodegen.ts&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Code generated&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PublisherCodegen&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;BaseEntity&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;readonly&lt;/span&gt;&lt;span&gt; logoColors&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EnumCollection&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Publisher&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Color&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hasEnumCollection&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;To the caller it looks like “an array of &lt;code dir=&quot;auto&quot;&gt;Color&lt;/code&gt;s”, but unlike an enum-array column it’s &lt;strong&gt;lazy&lt;/strong&gt; (because it lives in its own table, it’s not immediately fetched as part of the &lt;code dir=&quot;auto&quot;&gt;SELECT * FROM publishers&lt;/code&gt; call, so it needs a load hint), and it’s fully integrated with the rest of Joist:&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;publisher&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;(Publisher&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;p:1&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;logoColors&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;publisher&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;logoColors&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;// [Color.Red, Color.Blue]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;publisher&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;logoColors&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(Color&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Green&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;publisher&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;logoColors&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;([Color&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Green&lt;/span&gt;&lt;span&gt;, Color&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Blue&lt;/span&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; publisher&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;logoColors&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;includes&lt;/span&gt;&lt;span&gt;(Color&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Blue&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// probe without loading&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;It supports filtering (&lt;code dir=&quot;auto&quot;&gt;em.find(Publisher, { logoColors: Color.Red })&lt;/code&gt;), &lt;code dir=&quot;auto&quot;&gt;changes&lt;/code&gt; tracking, and reactivity, just like any other collection. See the &lt;a href=&quot;https://joist-orm.io/modeling/enum-tables/#enum-collections&quot;&gt;Enum Collections&lt;/a&gt; docs for when to reach for this versus a plain enum-array column.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;native-temporal-on-node-26&quot;&gt;Native Temporal on Node 26&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Joist &lt;a href=&quot;https://joist-orm.io/getting-started/configuration/#temporal&quot;&gt;supports the &lt;code dir=&quot;auto&quot;&gt;Temporal&lt;/code&gt; API&lt;/a&gt; for date/time columns, and historically required the &lt;code dir=&quot;auto&quot;&gt;temporal-polyfill&lt;/code&gt; package to provide it.&lt;/p&gt;
&lt;p&gt;Node 26 ships &lt;code dir=&quot;auto&quot;&gt;Temporal&lt;/code&gt; natively, so &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1889&quot;&gt;#1889&lt;/a&gt; updates Joist’s native-first / polyfill-fallback detection to transparently use the global &lt;code dir=&quot;auto&quot;&gt;Temporal&lt;/code&gt; when it’s available, falling back to the polyfill on older Node versions. There’s also a new &lt;code dir=&quot;auto&quot;&gt;import { Temporal } from &quot;joist-orm&quot;&lt;/code&gt; that resolves to whichever implementation is in use, so codegen and app code don’t need a direct import to either.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;optimized-batched-queries&quot;&gt;Optimized Batched Queries&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Two changes make Joist’s batched &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; queries leaner for the Postgres planner.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1909&quot;&gt;#1909&lt;/a&gt; adds &lt;strong&gt;constant inlining&lt;/strong&gt;. Previously, every filter value in a batch flowed through the &lt;code dir=&quot;auto&quot;&gt;_find&lt;/code&gt; CTE, even columns whose value was identical across all the batched calls. Now Joist detects when all batched queries share the same value for a column and inlines it as a plain bound predicate, leaving only the genuinely-varying columns in the 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; _find (tag, arg0) &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;span&gt; unnest($&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;[]), unnest($&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;character&lt;/span&gt;&lt;span&gt; varying[])&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; array_agg(&lt;/span&gt;&lt;span&gt;_find&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;tag&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; _tags, a.&lt;/span&gt;&lt;span&gt;*&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; authors &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; a&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CROSS JOIN&lt;/span&gt;&lt;span&gt; _find &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; _find&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;deleted_at&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;AND&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;3&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;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;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_find&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;arg0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;GROUP BY&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Here &lt;code dir=&quot;auto&quot;&gt;first_name = $3&lt;/code&gt; is a plain scalar comparison the planner can satisfy with an index, while only &lt;code dir=&quot;auto&quot;&gt;last_name&lt;/code&gt;–the column that actually differs between the batched queries–flows through the CTE.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1879&quot;&gt;#1879&lt;/a&gt; adds &lt;strong&gt;boolean expression pruning&lt;/strong&gt;, so a common conditional-filter idiom just works:&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;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;/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;name: { ilike: name &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;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; },&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;When &lt;code dir=&quot;auto&quot;&gt;name&lt;/code&gt; is falsy, the &lt;code dir=&quot;auto&quot;&gt;ilike: false&lt;/code&gt; term is pruned from the query entirely (rather than generating a spurious predicate), while equality operators like &lt;code dir=&quot;auto&quot;&gt;eq: false&lt;/code&gt; are preserved.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;many-to-many-tables-without-id-columns&quot;&gt;Many-to-Many Tables Without &lt;code dir=&quot;auto&quot;&gt;id&lt;/code&gt; Columns&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1904&quot;&gt;#1904&lt;/a&gt; lets Joist work with many-to-many join tables that don’t have a surrogate &lt;code dir=&quot;auto&quot;&gt;id&lt;/code&gt; primary key, i.e. tables whose primary key is just the &lt;code dir=&quot;auto&quot;&gt;(foo_id, bar_id)&lt;/code&gt; pair. This is a common shape for join tables, and Joist now handles loading, preloading, and mutating their rows just like the &lt;code dir=&quot;auto&quot;&gt;id&lt;/code&gt;-backed variant.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;performance-work&quot;&gt;Performance Work&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;2.3 includes a large round of hot-path performance work, benchmarked at 100k-entity scale across several independent process runs. The headline numbers (vs. the prior commit, all confirmed):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Faster em.flush dirty checking&lt;/strong&gt; (&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1857&quot;&gt;#1857&lt;/a&gt;): with a mostly-clean unit of work, scanning for dirty entities went from &lt;code dir=&quot;auto&quot;&gt;~4ms&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;~0.1ms&lt;/code&gt; when nothing dirty (&lt;code dir=&quot;auto&quot;&gt;~97%&lt;/code&gt;), and from &lt;code dir=&quot;auto&quot;&gt;~9.5ms&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;~4.8ms&lt;/code&gt; with 100 dirty entities (&lt;code dir=&quot;auto&quot;&gt;~49%&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Faster reaction walking&lt;/strong&gt; (&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1857&quot;&gt;#1857&lt;/a&gt;): insert-heavy flushes dropped &lt;code dir=&quot;auto&quot;&gt;~28-47%&lt;/code&gt; depending on column count.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lazier find indexes&lt;/strong&gt; (&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1861&quot;&gt;#1861&lt;/a&gt;): in-memory indexes are now maintained lazily per-field, and only triggered when fields are actually queried, which took a first-build scenario from &lt;code dir=&quot;auto&quot;&gt;~1517ms&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;~24ms&lt;/code&gt; (&lt;code dir=&quot;auto&quot;&gt;~98%&lt;/code&gt;), with &lt;code dir=&quot;auto&quot;&gt;30-50%&lt;/code&gt; steady-state wins.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Faster auto-batching key calc&lt;/strong&gt; (&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1859&quot;&gt;#1859&lt;/a&gt;): when deduping queries for N+1 prevention, we avoid re-JSON.stringify-ing keys, which dropped high-cost deduping by &lt;code dir=&quot;auto&quot;&gt;~52-58%&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reactive queue lookups&lt;/strong&gt; (&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1857&quot;&gt;#1857&lt;/a&gt;): &lt;code dir=&quot;auto&quot;&gt;~22-43%&lt;/code&gt; faster.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Entity hydration&lt;/strong&gt; and &lt;strong&gt;identity-map &lt;code dir=&quot;auto&quot;&gt;loadAll&lt;/code&gt;&lt;/strong&gt; paths: &lt;code dir=&quot;auto&quot;&gt;~17-34%&lt;/code&gt; faster.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Already-loaded &lt;code dir=&quot;auto&quot;&gt;populate&lt;/code&gt;&lt;/strong&gt; paths: &lt;code dir=&quot;auto&quot;&gt;~22-33%&lt;/code&gt; faster.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There’s also a new &lt;code dir=&quot;auto&quot;&gt;BENCHMARK-PLAN.md&lt;/code&gt; with the methodology and a regression-benchmark suite to keep these from regressing.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;smaller-but-notable-changes&quot;&gt;Smaller But Notable Changes&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A few other changes worth calling out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1900&quot;&gt;#1900&lt;/a&gt; aligns &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; handling of many-to-one and polymorphic behavior when passed empty arrays, &lt;code dir=&quot;auto&quot;&gt;em.find({ publisher: [] })&lt;/code&gt; now always means &lt;code dir=&quot;auto&quot;&gt;in: []&lt;/code&gt; which means “no rows”.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1883&quot;&gt;#1883&lt;/a&gt; excludes &lt;code dir=&quot;auto&quot;&gt;em.deleted&lt;/code&gt; entities from &lt;code dir=&quot;auto&quot;&gt;findCount&lt;/code&gt;, and &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1871&quot;&gt;#1871&lt;/a&gt; ensures &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt; does not return pending-delete entities.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1881&quot;&gt;#1881&lt;/a&gt; always defines &lt;code dir=&quot;auto&quot;&gt;transientFields&lt;/code&gt;, which is still only instantiated lazily, but useful for metaprogramming to rely on it.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1874&quot;&gt;#1874&lt;/a&gt; caches dataloader lookups/promises during &lt;code dir=&quot;auto&quot;&gt;em.populate&lt;/code&gt;, and &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1868&quot;&gt;#1868&lt;/a&gt; avoids recursive-property populate deadlocks.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1873&quot;&gt;#1873&lt;/a&gt; handles cycles in recursive Properties.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1891&quot;&gt;#1891&lt;/a&gt; skips preloading GraphQL fields with arguments (deferring to the resolver instead of unintentionally overloading data).&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1863&quot;&gt;#1863&lt;/a&gt; restores &lt;code dir=&quot;auto&quot;&gt;em.flush&lt;/code&gt; return order (broken in a previous perf refactoring), and &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1862&quot;&gt;#1862&lt;/a&gt; keeps &lt;code dir=&quot;auto&quot;&gt;loadLens&lt;/code&gt; synchronous for already-loaded paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are also misc correctness fixes around STI subtypes, recursive relations, reaction handling, and dependency bumps, plus codegen conflict-avoidance fixes (&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1901&quot;&gt;#1901&lt;/a&gt;, &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1902&quot;&gt;#1902&lt;/a&gt;, &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1903&quot;&gt;#1903&lt;/a&gt;) to keep generated scope/enum/type names from colliding with user symbols.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;upgrading&quot;&gt;Upgrading&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The main migration notes are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Replace &lt;code dir=&quot;auto&quot;&gt;em.findPaginated&lt;/code&gt; calls with &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt;, passing the same &lt;code dir=&quot;auto&quot;&gt;limit&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;offset&lt;/code&gt; options.&lt;/li&gt;
&lt;li&gt;After adding any &lt;code dir=&quot;auto&quot;&gt;scope&lt;/code&gt; declarations, re-run &lt;code dir=&quot;auto&quot;&gt;joist-codegen&lt;/code&gt; to refresh the generated &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;Entity&gt;Scopes&lt;/code&gt; types.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thanks to everyone filing issues, testing &lt;code dir=&quot;auto&quot;&gt;next&lt;/code&gt; releases, and pushing on Joist’s edge cases.&lt;/p&gt;
</content:encoded></item><item><title>Batched Paginated Finds</title><link>https://joist-orm.io/blog/batched-paginated-finds/</link><guid isPermaLink="true">https://joist-orm.io/blog/batched-paginated-finds/</guid><description>Joist now auto-batches paginated em.find calls, so GraphQL fields like reviews(first: 5) can be nested deep in a query without causing N+1s.</description><pubDate>Sun, 21 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Joist has always been aggressive about preventing N+1s–baking dataloader-style batching “into the data layer” was the original reason we started the Joist project back in ~2020.&lt;/p&gt;
&lt;p&gt;This auto-batching means that if you write code 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;// Loop over 100 authors&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; &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;(authors&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;async&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;=&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; 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;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Joist sees the repeated &lt;code dir=&quot;auto&quot;&gt;books.load()&lt;/code&gt; calls (&lt;em&gt;before&lt;/em&gt; they go on the wire), batches them together, and issues one &lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt; for all the authors’ books.&lt;/p&gt;
&lt;p&gt;Similarly, Joist’s &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; is batch-friendly, so if you have several semantically-similar &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt;s in flight at the same time:&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;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;([&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;find&lt;/span&gt;&lt;span&gt;(Book, { author: 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;em&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt;(Book, { author: 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;em&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt;(Book, { author: a3 }),&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;Joist turns that into one batched SQL query instead of three.&lt;/p&gt;
&lt;p&gt;One wrinkle was that, historically, this auto-batching did not work &lt;em&gt;paginated&lt;/em&gt; &lt;code dir=&quot;auto&quot;&gt;find&lt;/code&gt;s–i.e. &lt;code dir=&quot;auto&quot;&gt;em.find(..., { limit, offset })&lt;/code&gt;–until now! 🎉&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-graphql-use-case&quot;&gt;The GraphQL Use Case&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Joist is not a “GraphQL-only” ORM, but we’re definitely “GraphQL-informed”, so let’s look at a GraphQL schema where “lack of paginated finds” is problematic.&lt;/p&gt;
&lt;p&gt;Given a typical Author/Book/Review schema:&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; &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;books&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;!&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;type&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Book&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; Allow fetching a subset of the reviews. &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;reviews&lt;/span&gt;&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;&lt;span&gt;Int&lt;/span&gt;&lt;span&gt;): [&lt;/span&gt;&lt;span&gt;BookReview&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;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;first&lt;/code&gt; argument on &lt;code dir=&quot;auto&quot;&gt;reviews&lt;/code&gt; allows a client to “limit the book reviews” they retrieve:&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;query&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;author&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;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;books&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;reviews&lt;/span&gt;&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;&lt;span&gt;5&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;rating&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;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Normally Joist’s &lt;code dir=&quot;auto&quot;&gt;entityResolver&lt;/code&gt; can “put the entity on the wire” without any boilerplate, including relations like &lt;code dir=&quot;auto&quot;&gt;reviews&lt;/code&gt;, but because &lt;code dir=&quot;auto&quot;&gt;reviews&lt;/code&gt; has this custom &lt;code dir=&quot;auto&quot;&gt;first&lt;/code&gt; argument, we need implement it with &lt;code dir=&quot;auto&quot;&gt;em.find&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;export const &lt;/span&gt;&lt;span&gt;bookResolvers&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BookResolvers&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;entityResolver&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;getMetadata&lt;/span&gt;&lt;span&gt;(Book))&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;async &lt;/span&gt;&lt;span&gt;reviews&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;args&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ctx&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;// Normal/boring em.find 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;return &lt;/span&gt;&lt;span&gt;ctx&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;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;BookReview&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;book&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;{ limit: &lt;/span&gt;&lt;span&gt;args&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;undefined&lt;/span&gt;&lt;span&gt;, orderBy: { id: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;ASC&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;},&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;However, b/c the GraphQL runtime ends up invoking &lt;code dir=&quot;auto&quot;&gt;reviews&lt;/code&gt; function “in a loop”, an &lt;em&gt;unbatched&lt;/em&gt; implementation of this &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; would cause an N+1, as the query evaluation becomes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Load author &lt;code dir=&quot;auto&quot;&gt;a:1&lt;/code&gt; (good, 1 SQL call)&lt;/li&gt;
&lt;li&gt;Load all the author’s books (good, 1 SQL call, returns 100 books)&lt;/li&gt;
&lt;li&gt;For each book, load the first five reviews (eh, 100 SQL calls?)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That last line is the scary one. 😬&lt;/p&gt;
&lt;p&gt;If the author has 100 books, our initial resolver implementation will issue 100 &lt;code dir=&quot;auto&quot;&gt;SELECT ... FROM book_reviews WHERE book_id = ? LIMIT 5&lt;/code&gt; queries–we’ve created an N+1.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-pagination-is-different&quot;&gt;Why Pagination is Different&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If Joist is so great at auto-batching, why were these paginated &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; calls not batched already?&lt;/p&gt;
&lt;p&gt;For Joist’s &lt;em&gt;non-paginated&lt;/em&gt; one-to-many loading, batching is really straightforward: we use pretty much the same &lt;code dir=&quot;auto&quot;&gt;SELECT * FROM book_reviews&lt;/code&gt; that a non-batched &lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt; would do, but with a parameter of “an array for all requested book ids” instead of a single book id:&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; br.&lt;/span&gt;&lt;span&gt;*&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; book_reviews br&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;br&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; ANY($&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;ORDER BY&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;br&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;ASC&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The database returns all reviews for all books, and Joist groups them back by &lt;code dir=&quot;auto&quot;&gt;book_id&lt;/code&gt;, so we can populate each &lt;code dir=&quot;auto&quot;&gt;book&lt;/code&gt;s &lt;code dir=&quot;auto&quot;&gt;reviews&lt;/code&gt; collection.&lt;/p&gt;
&lt;p&gt;But &lt;code dir=&quot;auto&quot;&gt;reviews(first: 5)&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; mean “load any five reviews across all books”. It means “load five reviews &lt;em&gt;per&lt;/em&gt; book”, i.e. this SQL would be wrong:&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;-- Wrong SQL&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; br.&lt;/span&gt;&lt;span&gt;*&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; book_reviews br&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;br&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; ANY($&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;ORDER BY&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;br&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;ASC&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;LIMIT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;LIMIT 5&lt;/code&gt; applies to the whole result set, so if the first book has five reviews, books two through twenty get nothing.&lt;/p&gt;
&lt;p&gt;Given this wrinkle, i.e. it had not been immediately obvious “how to auto-batch paginated queries”, we’d so far pragmatically decided to just not even try. 😅&lt;/p&gt;
&lt;p&gt;So, historically, Joist didn’t allow &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; to accept pagination parameters (&lt;code dir=&quot;auto&quot;&gt;limit&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;offset&lt;/code&gt;), and instead we had a dedicated &lt;code dir=&quot;auto&quot;&gt;em.findPaginated&lt;/code&gt; that was &lt;em&gt;specifically not batched&lt;/em&gt;, but did accept &lt;code dir=&quot;auto&quot;&gt;limit&lt;/code&gt; &amp;#x26; &lt;code dir=&quot;auto&quot;&gt;offset&lt;/code&gt; parameters, which callers just had to know to “not call it in a loop”.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-new-pagination-sql&quot;&gt;The New Pagination SQL&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Until now! The latest Joist &lt;code dir=&quot;auto&quot;&gt;next&lt;/code&gt; release now batches paginated &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; calls.&lt;/p&gt;
&lt;p&gt;We achieve this by moving the &lt;code dir=&quot;auto&quot;&gt;LIMIT&lt;/code&gt; parameter from “a top-level &lt;code dir=&quot;auto&quot;&gt;LIMIT&lt;/code&gt;” to a “per book &lt;code dir=&quot;auto&quot;&gt;LIMIT&lt;/code&gt;” using a &lt;code dir=&quot;auto&quot;&gt;CROSS JOIN LATERAL&lt;/code&gt; statement, which looks somewhat cryptically like 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;WITH&lt;/span&gt;&lt;span&gt; _find (tag, arg0) &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;span&gt; unnest($&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;[]), unnest($&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;int&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;_find&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;tag&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; tag, _data.&lt;/span&gt;&lt;span&gt;*&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; _find &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; _find&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CROSS JOIN&lt;/span&gt;&lt;span&gt; LATERAL (&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; br.&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;FROM&lt;/span&gt;&lt;span&gt; book_reviews &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; br&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;br&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;_find&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;arg0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;ORDER BY&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;br&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;ASC&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;LIMIT&lt;/span&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;AS&lt;/span&gt;&lt;span&gt; _data&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The important pieces are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code dir=&quot;auto&quot;&gt;_find&lt;/code&gt; CTE lets pass us a small in-memory “lookup table” into Postgres, with one row for each of the batched calls
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;tag&lt;/code&gt; is just a counter of “batch 0”, “batch 1”, “batch 2”&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;arg0&lt;/code&gt; is the per-call &lt;code dir=&quot;auto&quot;&gt;book_id&lt;/code&gt;, i.e. &lt;code dir=&quot;auto&quot;&gt;book_id=10&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;book_id=20&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;book_id=30&lt;/code&gt; if we’re loading review for books &lt;code dir=&quot;auto&quot;&gt;[10, 20, 30]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;CROSS JOIN LATERAL&lt;/code&gt; runs the inner query once &lt;em&gt;per&lt;/em&gt; &lt;code dir=&quot;auto&quot;&gt;_find&lt;/code&gt; row.
&lt;ul&gt;
&lt;li&gt;This means that “each book” gets its own &lt;code dir=&quot;auto&quot;&gt;SELECT ... FROM book_reviews ... LIMIT&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code dir=&quot;auto&quot;&gt;LIMIT $3&lt;/code&gt; lives inside the lateral subquery, so it applies per book, not globally.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tying this back to our GraphQL use case, if a GraphQL query asks for &lt;code dir=&quot;auto&quot;&gt;reviews(first: 5)&lt;/code&gt; across 20 books, Postgres still receives one review query, and each book gets its own &lt;code dir=&quot;auto&quot;&gt;ORDER BY ... LIMIT 5&lt;/code&gt; semantics.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;removing-findpaginated&quot;&gt;Removing &lt;code dir=&quot;auto&quot;&gt;findPaginated&lt;/code&gt;&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Because we’ve now solved the restriction of “we cannot batch paginated finds”, the reason for separate &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; vs. &lt;code dir=&quot;auto&quot;&gt;em.findPaginated&lt;/code&gt; methods has gone away, so we’ve also just removed &lt;code dir=&quot;auto&quot;&gt;findPaginated&lt;/code&gt;. 🔪&lt;/p&gt;
&lt;p&gt;Now any &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; can use limit/offset parameters, and still get Joist’s robust auto-batching support:&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;aReviews&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;bReviews&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;&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;(BookReview, { book: b1 }, { limit: &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;, orderBy: { id: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;ASC&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;em&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt;(BookReview, { book: b2 }, { limit: &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;, orderBy: { id: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;ASC&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;As long as the paginated finds have compatible shapes–same entity, same options like &lt;code dir=&quot;auto&quot;&gt;limit&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;offset&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;orderBy&lt;/code&gt;, and the same logical filter shape–Joist will batch them.&lt;/p&gt;
&lt;p&gt;This first-class &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; support also means &lt;em&gt;any&lt;/em&gt; service-layer / business-logic code that loops over entities and asks for “the first &lt;code dir=&quot;auto&quot;&gt;N&lt;/code&gt; children” per entity will get an auto-batched query, even if no GraphQL is involved. 🎉&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-this-matters&quot;&gt;Why This Matters&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This feature finally unlocks a common GraphQL pattern: fetching “the first &lt;code dir=&quot;auto&quot;&gt;N&lt;/code&gt; children” for a list of parents, without having to write a custom dataloader/resolver or accept an N+1.&lt;/p&gt;
&lt;p&gt;Previously, applications using Joist had to choose between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Avoiding nested &lt;code dir=&quot;auto&quot;&gt;first&lt;/code&gt; fields all together (honestly what we’d done 😅),&lt;/li&gt;
&lt;li&gt;Writing custom dataloader resolvers by hand (👎️), or&lt;/li&gt;
&lt;li&gt;Accepting N+1s for “just a small first-five list” fields (also 👎️)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now the feature “just works”, with a straightforward, boring implementation that leverages &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt;, as we continue to deliver on Joist’s “never N+1” mission. 🚀&lt;/p&gt;
</content:encoded></item><item><title>Simplest Possible Closure Tables</title><link>https://joist-orm.io/blog/simplest-possible-closure-tables/</link><guid isPermaLink="true">https://joist-orm.io/blog/simplest-possible-closure-tables/</guid><description>Joist-driven implementation of closure tables as an example of our declarative business logic.</description><pubDate>Sun, 07 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Joist is a TypeScript ORM that lets you declaratively implement your business logic, which we explore in this post by examining this snippet of 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;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Employee&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;/** Create an employee -&gt; all transitive managers closure/m2m 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;readonly&lt;/span&gt;&lt;span&gt; managersClosure&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ReactiveManyToMany&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Employee&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Employee&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;hasReactiveManyToMany&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;&lt;/span&gt;&lt;span&gt;managersRecursive&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;e&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; [e, &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;managersRecursive&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;Which we assert is the simplest possible implementation of closure tables. 💥&lt;/p&gt;
&lt;p&gt;It achieves this by combining two of Joist’s primitives:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Recursive collections (the &lt;code dir=&quot;auto&quot;&gt;managersRecursive&lt;/code&gt; relation), and&lt;/li&gt;
&lt;li&gt;Reactive infrastructure like &lt;code dir=&quot;auto&quot;&gt;hasReactiveManyToMany&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We’ll look at each of these, and also explain closure tables while we’re at it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;closure-tables&quot;&gt;Closure Tables&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Closure tables are a niche but &lt;a href=&quot;https://www.percona.com/blog/moving-subtrees-in-closure-table/&quot;&gt;well&lt;/a&gt; &lt;a href=&quot;https://www.red-gate.com/simple-talk/&quot;&gt;known&lt;/a&gt; way to store hierarchical data (trees, or arbitrarily nested parent/child data) in a relational database that makes it easy to query “up or down multiple levels of the tree” with a single regular/fast join.&lt;/p&gt;
&lt;p&gt;For example, if we have an &lt;code dir=&quot;auto&quot;&gt;employees&lt;/code&gt; table with a &lt;code dir=&quot;auto&quot;&gt;manager_id&lt;/code&gt; column, some employees might have “just one manager” (they report directly to the CEO), while other employees might have “their manager, their manager’s manager, their manager’s manager’s manager”, etc. up to the CEO.&lt;/p&gt;
&lt;p&gt;Historically, given the “static” nature of SQL queries (i.e. no &lt;code dir=&quot;auto&quot;&gt;for&lt;/code&gt; loops), queries for “all the transitive managers for this employee” were difficult/impossible to query using just the &lt;code dir=&quot;auto&quot;&gt;manager_id&lt;/code&gt; column by itself.&lt;/p&gt;
&lt;aside aria-label=&quot;Recursive CTEs&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Recursive CTEs&lt;/p&gt;&lt;div&gt;&lt;p&gt;…at least until &lt;code dir=&quot;auto&quot;&gt;WITH RECURSIVE&lt;/code&gt; CTEs came along!&lt;/p&gt;&lt;p&gt;We’ll look at those soon.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;p&gt;Closure tables are a pattern of storing data that make these “find all transitive managers” queries easy.&lt;/p&gt;
&lt;p&gt;They do this by, at write-time when &lt;code dir=&quot;auto&quot;&gt;manager_id&lt;/code&gt; is changed, duplicating each hop from an employee to every single transitive manager as &lt;em&gt;derived&lt;/em&gt; data–they don’t replace the &lt;code dir=&quot;auto&quot;&gt;manager_id&lt;/code&gt; column, they’re &lt;em&gt;calculated from it&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;For an example like &lt;code dir=&quot;auto&quot;&gt;Fred&lt;/code&gt; reports to &lt;code dir=&quot;auto&quot;&gt;Bob&lt;/code&gt; reports to &lt;code dir=&quot;auto&quot;&gt;Jill&lt;/code&gt;, the closure table approach stores:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;fred.managersClosure=[fred, bob, jill]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;bob.managersClosure=[bob, jill]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;jill.managersClosure=[jill]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’m purposefully representing these as “each employee has a collection of ‘all their managers’” because I think that’s the easiest to mental model to reason about.&lt;/p&gt;
&lt;p&gt;In contrast, most closure table articles jump to “what does the &lt;code dir=&quot;auto&quot;&gt;managers_closure&lt;/code&gt; table look like in the database”, which is something like:&lt;/p&gt;
&lt;div padding:=&quot;&quot; &amp;#x27;24px&amp;#x27;=&quot;&quot; }}=&quot;&quot;&gt;
  &lt;img src=&quot;https://joist-orm.io/images/managers_closure_table_named_annotations.svg&quot;&gt;
&lt;/div&gt;
&lt;p&gt;Which is correct, but for me was hard to initially reason about.&lt;/p&gt;
&lt;p&gt;Instead, I think it’s most intuitive to articulate the “extra table” (the “closure table”) we’re adding as “each employee has a m2m between themselves and all their managers”–because that’s really what it is. 😅&lt;/p&gt;
&lt;p&gt;Now with this &lt;code dir=&quot;auto&quot;&gt;managers_closure&lt;/code&gt; table we can “find all managers” with a regular/boring SQL query with a single &lt;code dir=&quot;auto&quot;&gt;INNER JOIN&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;SELECT&lt;/span&gt;&lt;span&gt; e.&lt;/span&gt;&lt;span&gt;*&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; managers_closure mc&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; employees e &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&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;mc&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ancestor_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;mc&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;descendant_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; :employee_id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mc&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ancestor_id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mc&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;descendant_id&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;When we execute this query with &lt;code dir=&quot;auto&quot;&gt;:employee_id = fred&lt;/code&gt;, it returns two rows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;id=2 descendant_id=fred ancestor_id=bob&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;id=3 descendant_id=fred ancestor_id=jill&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And successfully returns “all of Fred’s managers” in a simple, fast query, regardless of “how many levels up” we had to go.&lt;/p&gt;
&lt;aside aria-label=&quot;Self rows&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Self rows&lt;/p&gt;&lt;div&gt;&lt;p&gt;You’ll notice the closure table pattern has each employee’s &lt;code dir=&quot;auto&quot;&gt;managersClosure&lt;/code&gt; relation also “refer to the employee themself”.&lt;/p&gt;&lt;p&gt;For the purposes of brevity, I’ll defer to the other closure pattern materials to explain the necessity of these “reflexive closure” rows.&lt;/p&gt;&lt;p&gt;We’re also omitting the &lt;code dir=&quot;auto&quot;&gt;depth&lt;/code&gt; column, since it’s not necessary for the use cases we personally are interested in.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;but-recursive-ctes&quot;&gt;…but Recursive CTEs!&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Most readers will probably have pointed out that “PostgreSQL solved this” by adding &lt;code dir=&quot;auto&quot;&gt;WITH RECURSIVE&lt;/code&gt; CTEs.&lt;/p&gt;
&lt;p&gt;Which is true!&lt;/p&gt;
&lt;p&gt;In modern PostgreSQL, we can often skip the &lt;code dir=&quot;auto&quot;&gt;managers_closure&lt;/code&gt; table with the following updated query, where the &lt;code dir=&quot;auto&quot;&gt;WITH RECURSIVE&lt;/code&gt; syntax basically acts as “the missing &lt;code dir=&quot;auto&quot;&gt;for&lt;/code&gt; loop” for us to walk up a variable number of levels, using only the &lt;code dir=&quot;auto&quot;&gt;manager_id&lt;/code&gt; 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;WITH&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;RECURSIVE&lt;/span&gt;&lt;span&gt; managers &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;-- anchor: the employee&apos;s direct manager&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; manager_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; employees&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; id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; :employee_id&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;UNION ALL&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;-- recursive: step up to each manager&apos;s manager&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;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;manager_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; employees e&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; managers m &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&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;m&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;manager_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;SELECT&lt;/span&gt;&lt;span&gt; manager_id&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; managers&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; manager_id &lt;/span&gt;&lt;span&gt;IS NOT NULL&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;I’ll defer to &lt;a href=&quot;https://medium.com/swlh/recursion-in-sql-explained-graphically-679f6a0f143b&quot;&gt;other blog posts&lt;/a&gt; to explain the syntax, but this is really great!&lt;/p&gt;
&lt;p&gt;Joist already has first-class support for &lt;code dir=&quot;auto&quot;&gt;RECURSIVE&lt;/code&gt; CTEs, in that anytime &lt;code dir=&quot;auto&quot;&gt;joist-codegen&lt;/code&gt; sees a “self-referential foreign-key” (i.e. the &lt;code dir=&quot;auto&quot;&gt;manager_id&lt;/code&gt; column that points back at its same &lt;code dir=&quot;auto&quot;&gt;employees&lt;/code&gt; table), we automatically create a &lt;code dir=&quot;auto&quot;&gt;...Recursive&lt;/code&gt; relation that is powered by a &lt;code dir=&quot;auto&quot;&gt;RECURSIVE&lt;/code&gt; CTE under the hood, i.e.:&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;fred&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;(Employee&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;e: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;// the regular m2o relation returns bob&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;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; fred&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;manager&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;// the recursive relation returns [bob, jill]&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;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; fred&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;managersRecursive&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;Where the &lt;code dir=&quot;auto&quot;&gt;managersRecursive.load()&lt;/code&gt; method will issue a SQL statement exactly like the &lt;code dir=&quot;auto&quot;&gt;WITH RECURSIVE&lt;/code&gt; CTE above, and get us all their transitive managers.&lt;/p&gt;
&lt;p&gt;And in stereotypical “never N+1” fashion, Joist will auto-batch the SQL so that we could load 100 or 1000 employees’ worth of &lt;code dir=&quot;auto&quot;&gt;managesRecursive&lt;/code&gt; and it would still be a single SQL query. 🎉&lt;/p&gt;
&lt;p&gt;Joist’s built-in support puts the power of &lt;code dir=&quot;auto&quot;&gt;RECURSIVE&lt;/code&gt; CTEs at your fingertips–just a &lt;code dir=&quot;auto&quot;&gt;load&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;populate&lt;/code&gt; call away. 😀&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-still-use-closure-tables&quot;&gt;Why Still Use Closure Tables?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;So, with this great PostgreSQL feature, are closure tables still applicable?&lt;/p&gt;
&lt;p&gt;For us, the answer is usually no–unless there is a very high-performance query to optimize. 🚀&lt;/p&gt;
&lt;p&gt;For example, internally we’re prototyping an RBAC-based auth system where permission grants “inherit” down a stack of “permission buckets”–like if Jill is the CEO, she can read the salary data of Bob and Fred, Bob can read the salary data of Fred, etc.&lt;/p&gt;
&lt;p&gt;In this setup, we’ll issue “what are your transitive auth permissions?” queries basically all the time, and need these queries to be extremely fast, i.e. easy for the Postgres query planner to optimize &amp;#x26; execute, which it is still better at doing for the “dumb/single join” of closure table queries, over recursive CTE queries.&lt;/p&gt;
&lt;p&gt;So, for this use case, we think it’s worth the write-time cost of generating an old-school &lt;code dir=&quot;auto&quot;&gt;permission_bucket_closures&lt;/code&gt; table, particularly if Joist can help us implement it as easily as possible.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;closure-table-maintenance&quot;&gt;Closure Table Maintenance&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;We skipped over one of the biggest cons of closure tables–keeping them up to date.&lt;/p&gt;
&lt;p&gt;If we have our same chain of &lt;code dir=&quot;auto&quot;&gt;Fred&lt;/code&gt; is managed by &lt;code dir=&quot;auto&quot;&gt;Bob&lt;/code&gt; is managed &lt;code dir=&quot;auto&quot;&gt;Jill&lt;/code&gt; (that resulted in the 6 &lt;code dir=&quot;auto&quot;&gt;managers_closure&lt;/code&gt; rows in the image above), but now we add &lt;code dir=&quot;auto&quot;&gt;Jan&lt;/code&gt; as a layer of management between &lt;code dir=&quot;auto&quot;&gt;Bob&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;Jill&lt;/code&gt;, we have a lot of bookkeeping to update.&lt;/p&gt;
&lt;p&gt;Specifically we need to add four rows:&lt;/p&gt;
&lt;div padding:=&quot;&quot; &amp;#x27;24px&amp;#x27;=&quot;&quot; }}=&quot;&quot;&gt;
  &lt;img src=&quot;https://joist-orm.io/images/managers_closure_after_inserting_jan.svg&quot;&gt;
&lt;/div&gt;
&lt;p&gt;Which can be intuitively explained as:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;All managers above Jan need Jan as a new report&lt;/li&gt;
&lt;li&gt;All employees below Jan need Jan as a new boss,&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This doesn’t seem too bad–but what if the hierarchy had more than just a single &lt;code dir=&quot;auto&quot;&gt;bob.manager = jan&lt;/code&gt; mutation? What if several relationships changed at once?&lt;/p&gt;
&lt;p&gt;It starts to get complicated to make it “just work”–until we lean into Joist’s reactivity.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;joist-reactivity-for-the-win&quot;&gt;Joist Reactivity for the Win&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Let’s look at the two updates we need for “Jan is a new layer of management”:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;All managers above Jan need Jan as a new report&lt;/li&gt;
&lt;li&gt;All employees below Jan need Jan as a new boss,&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The first important insight is to lean into our mental model of “closure tables are just fancy m2m collections”, such that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;All managers above Jan need Jan as a new report –&gt;
&lt;ul&gt;
&lt;li&gt;This is really saying set &lt;code dir=&quot;auto&quot;&gt;jan.managersClosure=[jan, jill]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;I.e. calcing the m2m for Jan herself will “update the managers above her” because it is “Jan’s m2m collection” that fundamentally controls “who are all of her transitive managers”?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;All employees below Jan need Jan as a new boss –&gt;
&lt;ul&gt;
&lt;li&gt;This is really saying recalc &lt;code dir=&quot;auto&quot;&gt;[fred, bob].managersClosure=[..., jan]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;I.e. re-calcing the m2m for “Jan’s transitive reports” to let them see “now Jan is also a manager of me”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And the second insight is that “recalculating the m2m all of Jan’s &lt;em&gt;downstream&lt;/em&gt; reports” is exactly what Joist’s reactivity does when it sees a &lt;code dir=&quot;auto&quot;&gt;employee.manager&lt;/code&gt; relationship change–it knows &lt;code dir=&quot;auto&quot;&gt;bob.manager&lt;/code&gt; changed, hence &lt;code dir=&quot;auto&quot;&gt;bob.managersRecursive&lt;/code&gt; changed, hence anyone “watching &lt;code dir=&quot;auto&quot;&gt;bob.managersRecursive&lt;/code&gt;” which requires “looking &lt;em&gt;down&lt;/em&gt; instead of &lt;em&gt;up&lt;/em&gt;” needs to be told about the change.&lt;/p&gt;
&lt;p&gt;All of this is achieved by our &lt;code dir=&quot;auto&quot;&gt;managersClosure&lt;/code&gt; declaration:&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;readonly managersClosure: ReactiveManyToMany&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Employee, Employee&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;span&gt;  &lt;/span&gt;&lt;span&gt;hasReactiveManyToMany&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;&lt;/span&gt;&lt;span&gt;managersRecursive&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;e&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; [e, &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;managersRecursive&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;When the &lt;code dir=&quot;auto&quot;&gt;jan.manager = jill&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;bob.manager = jan&lt;/code&gt; graph mutation happens, Joist does roughly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Setting &lt;code dir=&quot;auto&quot;&gt;jan.manager = jill&lt;/code&gt; triggers reactivity on “who is watching this write?”
&lt;ul&gt;
&lt;li&gt;We see &lt;code dir=&quot;auto&quot;&gt;managersClosure&lt;/code&gt; is watching &lt;code dir=&quot;auto&quot;&gt;jan.managersRecursive&lt;/code&gt;, and “reverse the hint” to find employee’s &lt;em&gt;below&lt;/em&gt; Jan that need to know about this change&lt;/li&gt;
&lt;li&gt;This queues &lt;code dir=&quot;auto&quot;&gt;[jan, bob, fred]&lt;/code&gt; to have their &lt;code dir=&quot;auto&quot;&gt;managersClosure&lt;/code&gt; recalc-d&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Setting &lt;code dir=&quot;auto&quot;&gt;bob.manager = jan&lt;/code&gt; also triggers “who is watching this write?”
&lt;ul&gt;
&lt;li&gt;We again see &lt;code dir=&quot;auto&quot;&gt;managersClosure&lt;/code&gt; is watching &lt;code dir=&quot;auto&quot;&gt;bob.managersRecursive&lt;/code&gt;, and “reverse the hint” to find employee’s &lt;em&gt;below&lt;/em&gt; Bob&lt;/li&gt;
&lt;li&gt;This queues &lt;code dir=&quot;auto&quot;&gt;[bob, fred]&lt;/code&gt; to have their &lt;code dir=&quot;auto&quot;&gt;managersClosure&lt;/code&gt; recalc-d
&lt;ul&gt;
&lt;li&gt;…technically this is a noop b/c they’re already queued&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;We recalc the &lt;code dir=&quot;auto&quot;&gt;[jan, bob, fred].managersClosure&lt;/code&gt; relations all at once
&lt;ul&gt;
&lt;li&gt;Joist resolves the &lt;code dir=&quot;auto&quot;&gt;managersRecursive&lt;/code&gt; reactive hint for all three entities
&lt;ul&gt;
&lt;li&gt;If any relations are already loaded, we use the in-memory results&lt;/li&gt;
&lt;li&gt;If any relations are not loaded, we issue a single batched CTE query for their data&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;When the batched SQL resolves, the &lt;code dir=&quot;auto&quot;&gt;managersClosure&lt;/code&gt; lambda is invoked for each of &lt;code dir=&quot;auto&quot;&gt;[jan, bob, fred]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;This sets &lt;code dir=&quot;auto&quot;&gt;jan.managersClosure = [jan, jill]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;And sets &lt;code dir=&quot;auto&quot;&gt;bob.managersClosure = [bob, jan, jill]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;And sets &lt;code dir=&quot;auto&quot;&gt;fred.managersClosure = [fred, bob, jan, jill]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;We call &lt;code dir=&quot;auto&quot;&gt;em.flush()&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;The three mutated &lt;code dir=&quot;auto&quot;&gt;managerClosures&lt;/code&gt; m2m collections are diffed&lt;/li&gt;
&lt;li&gt;We issue a single &lt;code dir=&quot;auto&quot;&gt;INSERT INTO managers_closure&lt;/code&gt; with the new rows&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is admittedly a lot 😵, but thankfully it’s all driven by Joist’s internal infra keeping the &lt;code dir=&quot;auto&quot;&gt;hasReactiveManyToMany&lt;/code&gt; m2m up-to-date, by respecting the &lt;code dir=&quot;auto&quot;&gt;managersRecursive&lt;/code&gt; reactive hint we declared in the code.&lt;/p&gt;
&lt;p&gt;And the key outcome is that the closure table was &lt;em&gt;completely updated for all affected employees&lt;/em&gt;, and &lt;strong&gt;we only issued 3 SQL calls&lt;/strong&gt;, 2 reads and 1 write:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A single “down the tree” &lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt; to find the employees to recalc&lt;/li&gt;
&lt;li&gt;A single “up the tree” &lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt; to load the data for each lambda&lt;/li&gt;
&lt;li&gt;A single &lt;code dir=&quot;auto&quot;&gt;INSERT&lt;/code&gt; the new m2m rows.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And because each of these operations is fundamentally batched, it will be the same 3 SQL calls &lt;em&gt;if we’re changing 1 employee/manager relation or 100 employee/manager relationships&lt;/em&gt;. 🤯&lt;/p&gt;
&lt;p&gt;A pretty amazing result for a 1-liner–can you imagine trying to achieve this with an adhoc, hand-coded implementation? It’s not something I would readily sign up for. 😬&lt;/p&gt;
&lt;aside aria-label=&quot;Absolute Minimum Queries&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Absolute Minimum Queries&lt;/p&gt;&lt;div&gt;&lt;p&gt;As a disclaimer, our approach does issue two reads / &lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt;s before knowing the &lt;code dir=&quot;auto&quot;&gt;INSERT&lt;/code&gt; to update.&lt;/p&gt;&lt;p&gt;Most of the traditional, SQL-oriented posts like &lt;a href=&quot;https://www.percona.com/blog/moving-subtrees-in-closure-table/&quot;&gt;the one we linked above&lt;/a&gt; have examples of “jumping to the &lt;code dir=&quot;auto&quot;&gt;INSERT&lt;/code&gt;s / &lt;code dir=&quot;auto&quot;&gt;DELETE&lt;/code&gt;s writes”, without any initial &lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt;s, by sussing out the algorithm (i.e. non-trivial &lt;code dir=&quot;auto&quot;&gt;WHERE&lt;/code&gt; clauses) for keeping the m2m/closure table rows in sync across mutations.&lt;/p&gt;&lt;p&gt;This “jump to writes” approach avoids our two up-front &lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt;s, so we must concede that Joist’s approach is not “the absolute minimum number of queries”, and we do pay the cost of pulling all &lt;code dir=&quot;auto&quot;&gt;employees&lt;/code&gt; in the path into memory.&lt;/p&gt;&lt;p&gt;If you have extremely deep (1000+ level?) trees, this next level of optimization could be worth it, albeit it would come with the increased complexity.&lt;/p&gt;&lt;p&gt;Our assertion is that Joist’s performance of “3 fixed queries” will pragmatically be more performant than all but the most hand-optimized implementations, and its low-cost/simplicity means the ROI often wins over a custom approach.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;That is our deep-dive of how two Joist primitives, recursive CTE support &amp;#x26; reactive m2m relations, can combine to drive a really powerful result–closure tables implemented as a 1-liner.&lt;/p&gt;
&lt;p&gt;The neatest aspect, to us, is that when we started our internal RBAC auth system, we did not need to “build closure tables as a new feature of Joist”–instead the solution emerged elegantly as just a combination of our existing features.&lt;/p&gt;
&lt;p&gt;To be clear, we’re not expecting readers to rush out and implement closure tables in their application–it is still a niche pattern, and, as we’ve seen, Postgres’s recursive CTEs provide a great modern alternative (which Joist’s recursive relations also put at your fingertips).&lt;/p&gt;
&lt;p&gt;Instead our purpose is illustrating the power of Joist’s “declarative business logic” and “reactive calculations”, and encouraging readers to think about how these features could make their lives easier in their own work.&lt;/p&gt;
</content:encoded></item><item><title>Joist 2.2 Released!</title><link>https://joist-orm.io/blog/joist-2-2/</link><guid isPermaLink="true">https://joist-orm.io/blog/joist-2-2/</guid><description>Joist 2.2 is out, including both the 2.1 and 2.2 releases with faster collection queries, soft-delete resurrection, recursive many-to-many relations, unloaded collection sets, and the node-pg driver migration.</description><pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Joist 2.2 is out, and we didn’t get around to a 2.1 release announcement 😬, so we’re going to cover both the &lt;a href=&quot;https://github.com/joist-orm/joist-orm/commit/cc1eaf3b40e8a4d34f710de9146ecddca33bab14&quot;&gt;2.1&lt;/a&gt; and &lt;a href=&quot;https://github.com/joist-orm/joist-orm/commit/a6e65e05915463b38e82adc3965e4352681a39c3&quot;&gt;2.2&lt;/a&gt; releases.&lt;/p&gt;
&lt;p&gt;As usual, Joist releases are driven by the problems we’re solving in our day-to-day application work at &lt;a href=&quot;https://www.homebound.com/&quot;&gt;Homebound&lt;/a&gt;, so this release has a mix of headline features, API cleanup, performance work, and correctness fixes.&lt;/p&gt;
&lt;p&gt;The biggest changes are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Collection filters now use &lt;code dir=&quot;auto&quot;&gt;EXISTS&lt;/code&gt; clauses, avoiding join fanout &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; queries&lt;/li&gt;
&lt;li&gt;Joist’s Postgres driver now uses &lt;code dir=&quot;auto&quot;&gt;node-pg&lt;/code&gt; directly instead of Knex&lt;/li&gt;
&lt;li&gt;Soft-deleted rows can now be resurrected by &lt;code dir=&quot;auto&quot;&gt;upsert&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;findOrCreate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Recursive relations now support self-referential many-to-many tables&lt;/li&gt;
&lt;li&gt;Derived property APIs were renamed, with new async-capable properties&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;collection.set(...)&lt;/code&gt; now works without preloading the collection&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;faster-collection-queries-with-exists&quot;&gt;Faster Collection Queries with &lt;code dir=&quot;auto&quot;&gt;EXISTS&lt;/code&gt;&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The biggest 2.2 change is &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1743&quot;&gt;#1743&lt;/a&gt;, which changes how Joist renders many collection filters.&lt;/p&gt;
&lt;p&gt;Previously, a query like 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;// Find authors&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;find&lt;/span&gt;&lt;span&gt;(Author, {&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 they have a &quot;child book&quot; with this 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;books: { title: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Domain Modeling&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;// And also a &quot;child comment&quot; with this text&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;comments: { text: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;great post&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;Would result in multiple &lt;code dir=&quot;auto&quot;&gt;LEFT JOIN&lt;/code&gt;s into the &lt;code dir=&quot;auto&quot;&gt;books&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;comments&lt;/code&gt; tables, followed by &lt;code dir=&quot;auto&quot;&gt;DISTINCT&lt;/code&gt; to collapse duplicate &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; rows.&lt;/p&gt;
&lt;p&gt;That worked, but can create a lot of intermediate rows: if an author has 10 books and 20 comments, the database can end up considering 200 joined rows for that one author before deduping. Usually this fan out didn’t matter–until the combined rows got really big, at which point it could adversely affect performance on the database server.&lt;/p&gt;
&lt;p&gt;Joist now renders collection filters as correlated &lt;code dir=&quot;auto&quot;&gt;EXISTS&lt;/code&gt; subqueries where possible:&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;WHERE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EXISTS&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;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;FROM&lt;/span&gt;&lt;span&gt; books b&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;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;span&gt; &lt;/span&gt;&lt;span&gt;AND&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;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Domain Modeling&lt;/span&gt;&lt;span&gt;&apos;&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;AND&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EXISTS&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;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;FROM&lt;/span&gt;&lt;span&gt; comments c&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;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parent_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;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;text&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;great post&lt;/span&gt;&lt;span&gt;&apos;&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;The original PR measured this from &lt;code dir=&quot;auto&quot;&gt;1.4x&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;10.9x&lt;/code&gt; faster than join-plus-distinct in benchmarked cases, with one match-all case reducing planner rows processed by up to &lt;code dir=&quot;auto&quot;&gt;542x&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There are a few behavior changes worth calling out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Queries that still require multiple collection &lt;code dir=&quot;auto&quot;&gt;LEFT JOIN&lt;/code&gt;s (due to having complex &lt;code dir=&quot;auto&quot;&gt;WHERE&lt;/code&gt; conditions that &lt;code dir=&quot;auto&quot;&gt;OR&lt;/code&gt; across &lt;em&gt;both&lt;/em&gt; child collections in a single clause) may now throw unless &lt;code dir=&quot;auto&quot;&gt;allowMultipleLeftJoins: true&lt;/code&gt; is set.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;beforeFind&lt;/code&gt; plugins still see the “pre-&lt;code dir=&quot;auto&quot;&gt;EXISTS&lt;/code&gt;” AST – this lets the plugin stay focused on the &lt;code dir=&quot;auto&quot;&gt;LEFT JOIN&lt;/code&gt; as the “logical child collection” and let Joist do any &lt;code dir=&quot;auto&quot;&gt;EXISTS&lt;/code&gt; optimizations after the fact&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;goodbye-knex-hello-node-pg&quot;&gt;Goodbye Knex, Hello &lt;code dir=&quot;auto&quot;&gt;node-pg&lt;/code&gt;&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The largest 2.1 breaking change is &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1754&quot;&gt;#1754&lt;/a&gt;: Joist’s &lt;code dir=&quot;auto&quot;&gt;PostgresDriver&lt;/code&gt; now uses &lt;code dir=&quot;auto&quot;&gt;node-pg&lt;/code&gt; directly instead of taking a Knex instance.&lt;/p&gt;
&lt;p&gt;New setup 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;import&lt;/span&gt;&lt;span&gt; pg &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;pg&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;import&lt;/span&gt;&lt;span&gt; { PostgresDriver } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;joist-orm&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;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;pool&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;pg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Pool&lt;/span&gt;&lt;span&gt;(config);&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;driver&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;PostgresDriver&lt;/span&gt;&lt;span&gt;(pool&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;onQuery&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;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&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;(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;},&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If you still want Knex for custom query building (i.e. starting with an &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; to build 90% of your query, and then customize the last 10%–a common pattern we use internally), it is still available via &lt;code dir=&quot;auto&quot;&gt;joist-orm/knex&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;import&lt;/span&gt;&lt;span&gt; { createKnex } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;joist-orm/knex&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;const &lt;/span&gt;&lt;span&gt;knex&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;createKnex&lt;/span&gt;&lt;span&gt;(pool);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;aside aria-label=&quot;Caution&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Caution&lt;/p&gt;&lt;div&gt;&lt;p&gt;This is a breaking change for driver setup: &lt;code dir=&quot;auto&quot;&gt;PostgresDriver&lt;/code&gt; now takes a &lt;code dir=&quot;auto&quot;&gt;pg.Pool&lt;/code&gt;, and transaction-level APIs use &lt;code dir=&quot;auto&quot;&gt;pg.PoolClient&lt;/code&gt; instead of &lt;code dir=&quot;auto&quot;&gt;Knex.Transaction&lt;/code&gt;.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;soft-delete-resurrection&quot;&gt;Soft-Delete Resurrection&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Soft deletes are great until a unique constraint says “you already have a row for that” and your operation blows up.&lt;/p&gt;
&lt;p&gt;Joist 2.2 adds “resurrection” support in &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1802&quot;&gt;#1802&lt;/a&gt;, with follow-up fixes in &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1821&quot;&gt;#1821&lt;/a&gt; and &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1822&quot;&gt;#1822&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, when &lt;code dir=&quot;auto&quot;&gt;upsert&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;findOrCreate&lt;/code&gt; would create a new row, Joist first checks for a “matching” soft-deleted row. If one exists, Joist clears it’s &lt;code dir=&quot;auto&quot;&gt;deleted_at&lt;/code&gt; columns, and returns/updates that row instead:&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;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;upsert&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;ssn: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;123-45-6789&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;firstName: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Jane&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;lastName: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Austen&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;In this example, if a soft-deleted &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; with &lt;code dir=&quot;auto&quot;&gt;ssn = &quot;123-45-6789&quot;&lt;/code&gt; already exists, Joist resurrects that existing row instead of trying to insert a new one.&lt;/p&gt;
&lt;p&gt;For &lt;code dir=&quot;auto&quot;&gt;upsert&lt;/code&gt;, the resurrection identity comes from &lt;code dir=&quot;auto&quot;&gt;uniqueBy&lt;/code&gt; in the &lt;code dir=&quot;auto&quot;&gt;joist-config.json&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;{&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;entities&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;Author&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;uniqueBy&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;ssn&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;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;Book&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;uniqueBy&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;author&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;title&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;}&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;Resurrection is intentionally treated as an update:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The existing id is preserved&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;beforeUpdate&lt;/code&gt; runs&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;beforeCreate&lt;/code&gt; does not run&lt;/li&gt;
&lt;li&gt;Ordinary duplicate rows still fail as expected&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;recursive-many-to-many-relations&quot;&gt;Recursive Many-to-Many Relations&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Joist has supported recursive relations for self-referential foreign keys, i.e. an &lt;code dir=&quot;auto&quot;&gt;employee.manager.get&lt;/code&gt; will also have a &lt;code dir=&quot;auto&quot;&gt;employee.managersRecursive.get&lt;/code&gt; backed by Postgres recursive CTE queries.&lt;/p&gt;
&lt;p&gt;Joist 2.1 extends that support to self-referential many-to-many tables in &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1739&quot;&gt;#1739&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For example, if users can have many parent users through a join table, Joist can now generate recursive collections for the full ancestry/descendant graph:&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 [u5] has parents [u4, u3]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// And parent [u4] has parents [u2, u1]&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;// When accesing u5&apos;s parentsRecursive&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;u5&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;(User&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;u:5&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;parentsRecursive&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;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Then we see u4, u3, u2, 1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;u5&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parentsRecursive&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// [u4, u4, u2, u1]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Recursive m2m collections support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Batched loading for multiple seed entities.&lt;/li&gt;
&lt;li&gt;Respect in-memory, unflushed changes.&lt;/li&gt;
&lt;li&gt;Cycle detection.&lt;/li&gt;
&lt;li&gt;Reactive hints, added in &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1744&quot;&gt;#1744&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cycle handling was also cleaned up and documented in &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1745&quot;&gt;#1745&lt;/a&gt;, &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1746&quot;&gt;#1746&lt;/a&gt;, and &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1747&quot;&gt;#1747&lt;/a&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;config&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addCycleRule&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;&lt;/span&gt;&lt;span&gt;parentsRecursive&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;u&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;`&lt;/span&gt;&lt;span&gt;User &lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt; has a cycle in their parent hierarchy&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;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;derived-property-api-cleanup&quot;&gt;Derived Property API Cleanup&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;2.2 also cleans up Joist’s derived-property naming in &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1789&quot;&gt;#1789&lt;/a&gt; and &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1819&quot;&gt;#1819&lt;/a&gt;, while adding new SQL-backed derived properties in &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1781&quot;&gt;#1781&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The old &lt;code dir=&quot;auto&quot;&gt;hasAsyncProperty&lt;/code&gt; API is now &lt;code dir=&quot;auto&quot;&gt;hasProperty&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;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;readonly&lt;/span&gt;&lt;span&gt; numberOfBooks &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hasProperty&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; 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;/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;/figure&gt;&lt;/div&gt;
&lt;p&gt;The rationale is that properties like &lt;code dir=&quot;auto&quot;&gt;numberOfBooks&lt;/code&gt; are “just properties” because their lambda is synchronous, and the &lt;code dir=&quot;auto&quot;&gt;hasAsyncProperty&lt;/code&gt; term (their previous name) is better reserved for properties that use the &lt;code dir=&quot;auto&quot;&gt;async&lt;/code&gt; keyword for their lambda.&lt;/p&gt;
&lt;p&gt;These new “actually async” &lt;code dir=&quot;auto&quot;&gt;hasAsyncProperty&lt;/code&gt;’s, with their lambda being &lt;code dir=&quot;auto&quot;&gt;async&lt;/code&gt;, means they can use async database queries to their calculate values:&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;Publisher&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;PublisherCodegen&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;readonly&lt;/span&gt;&lt;span&gt; numberOfBookReviews &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hasAsyncProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &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;&lt;/span&gt;&lt;span&gt;p&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;findCount&lt;/span&gt;&lt;span&gt;(BookReview, { book: { author: { publisher: p&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;Similarly, &lt;code dir=&quot;auto&quot;&gt;ReactiveQueryField&lt;/code&gt; is now named &lt;code dir=&quot;auto&quot;&gt;AsyncReactiveField&lt;/code&gt;, for reactive fields whose recalculation is backed by a database 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;Publisher&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;PublisherCodegen&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;readonly&lt;/span&gt;&lt;span&gt; numberOfBookReviews &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hasAsyncReactiveField&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;&lt;/span&gt;&lt;span&gt;id&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;{ authors: { books: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;reviews&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;p&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; p&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;findCount&lt;/span&gt;&lt;span&gt;(BookReview, { book: { author: { publisher: p&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&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;/figure&gt;&lt;/div&gt;
&lt;aside aria-label=&quot;Caution&quot;&gt;&lt;p aria-hidden=&quot;true&quot;&gt;Caution&lt;/p&gt;&lt;div&gt;&lt;p&gt;This is a breaking rename: old &lt;code dir=&quot;auto&quot;&gt;hasAsyncProperty&lt;/code&gt; usages should become &lt;code dir=&quot;auto&quot;&gt;hasProperty&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;ReactiveQueryField&lt;/code&gt; usages should become &lt;code dir=&quot;auto&quot;&gt;AsyncReactiveField&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Running &lt;code dir=&quot;auto&quot;&gt;yarn joist-codegen&lt;/code&gt; will automatically prompt you to apply codemods for this change, which were added and fixed in &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1791&quot;&gt;#1791&lt;/a&gt;, &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1794&quot;&gt;#1794&lt;/a&gt;, and &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1796&quot;&gt;#1796&lt;/a&gt;.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;collectionset-works-unloaded&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;collection.set&lt;/code&gt; Works Unloaded&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;In 2.1, &lt;code dir=&quot;auto&quot;&gt;set&lt;/code&gt; no longer requires a preloaded collection for either many-to-many (&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1722&quot;&gt;#1722&lt;/a&gt;) or one-to-many (&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1725&quot;&gt;#1725&lt;/a&gt;) relations.&lt;/p&gt;
&lt;p&gt;This means you can replace a collection directly:&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;book&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;(Book&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;b:2&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;const [&lt;/span&gt;&lt;span&gt;t4&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;t5&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;loadAll&lt;/span&gt;&lt;span&gt;(Tag&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;t:4&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;t:5&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;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;book&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;tags&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;([t4, t5]);&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;Without first doing:&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;await&lt;/span&gt;&lt;span&gt; book&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;tags&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;Joist tracks the intended final set, flushes the diff, composes later &lt;code dir=&quot;auto&quot;&gt;.add&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;.remove&lt;/code&gt; calls correctly, and still triggers reactive validation rules from the unloaded set.&lt;/p&gt;
&lt;p&gt;For one-to-many relations, Joist also percolates the many-to-one side immediately for loaded or new children:&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;publisher&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;(Publisher&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;p: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;const [&lt;/span&gt;&lt;span&gt;a2&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;a3&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;loadAll&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;&quot;&lt;/span&gt;&lt;span&gt;a:2&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;a:3&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;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;publisher&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;set&lt;/span&gt;&lt;span&gt;([a2, a3]);&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;This is a small API change but big DX win: most &lt;code dir=&quot;auto&quot;&gt;set&lt;/code&gt; calls were already saying “make the final collection exactly this”, so requiring a pre-load was unnecessary noise.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;entitymanagerpendingchanges&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;EntityManager.pendingChanges&lt;/code&gt;&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;2.1 adds a new debugging/tooling API in &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1748&quot;&gt;#1748&lt;/a&gt;: &lt;code dir=&quot;auto&quot;&gt;em.pendingChanges&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It exposes a high-level snapshot of the unflushed work in the current unit of work:&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;newAuthor&lt;/span&gt;&lt;span&gt;(em);&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;em&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pendingChanges&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// [{ kind: &quot;create&quot;, entity: a }]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;It also includes relation changes:&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;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;tags&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(t);&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;em&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pendingChanges&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// [{ kind: &quot;m2m&quot;, op: &quot;add&quot;, joinTableName: &quot;authors_to_tags&quot;, entities: [a, t] }]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This is especially useful for tests, debugging tools, audit-style logic, or any code that wants to inspect what &lt;code dir=&quot;auto&quot;&gt;flush()&lt;/code&gt; is about to do without poking at internal state.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;markdown-backed-entity-docs&quot;&gt;Markdown-Backed Entity Docs&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1749&quot;&gt;#1749&lt;/a&gt; adds codegen support for syncing entity documentation between Markdown files and generated TypeScript docs.&lt;/p&gt;
&lt;p&gt;The new config options are:&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;{&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;docs&quot;&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;&quot;outputDocs&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;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;With this enabled, entity docs can live in Markdown:&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;## Overview&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 Author entity represents a writer who can publish books.&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;## Fields&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;### numberOfBooks&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 number of books this author has written.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;And codegen can sync them into JSDoc and emit runtime metadata:&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 const &lt;/span&gt;&lt;span&gt;docs&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;/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;comment: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;The Author entity represents a writer who can publish 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;fields: {&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;numberOfBooks: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;The number of books this author has written.&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;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} as const&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This should make it easier to keep entity docs readable in-repo while still making them available to GraphQL, admin UIs, or other runtime tooling.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;smaller-but-notable-changes&quot;&gt;Smaller But Notable Changes&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A few other changes are worth calling out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1762&quot;&gt;#1762&lt;/a&gt; adds &lt;code dir=&quot;auto&quot;&gt;is&lt;/code&gt; aliases in test factories, so tests can assert stable factory ids and reference in-flight entities by factory id.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1718&quot;&gt;#1718&lt;/a&gt; fixes &lt;code dir=&quot;auto&quot;&gt;findOrCreate&lt;/code&gt; support for &lt;code dir=&quot;auto&quot;&gt;null&lt;/code&gt; values.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1758&quot;&gt;#1758&lt;/a&gt; removes the implicit &lt;code dir=&quot;auto&quot;&gt;entityLimit&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;DISTINCT&lt;/code&gt; from &lt;code dir=&quot;auto&quot;&gt;findIds&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1784&quot;&gt;#1784&lt;/a&gt; and &lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1786&quot;&gt;#1786&lt;/a&gt; make m2m ordering deterministic across loading paths and processes.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1798&quot;&gt;#1798&lt;/a&gt; avoids stack overflows in &lt;code dir=&quot;auto&quot;&gt;BatchLoader.loadAll&lt;/code&gt; with very large key arrays.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1820&quot;&gt;#1820&lt;/a&gt; adds plugin hooks around validation: &lt;code dir=&quot;auto&quot;&gt;beforeValidate&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;afterValidate&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joist-orm/joist-orm/pull/1825&quot;&gt;#1825&lt;/a&gt; runs default values in Joist’s trusted context, so plugins/field logging don’t treat default initialization as user writes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are also many correctness fixes in reaction handling, load hints, subtype matching, stale preload caches, soft-delete joins, docs formatting, and dependency bumps across both release trains.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;upgrading&quot;&gt;Upgrading&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The main migration notes are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Update &lt;code dir=&quot;auto&quot;&gt;PostgresDriver&lt;/code&gt; setup to pass a &lt;code dir=&quot;auto&quot;&gt;pg.Pool&lt;/code&gt; instead of Knex.&lt;/li&gt;
&lt;li&gt;Rename old &lt;code dir=&quot;auto&quot;&gt;hasAsyncProperty&lt;/code&gt; usages to &lt;code dir=&quot;auto&quot;&gt;hasProperty&lt;/code&gt; (codemod).&lt;/li&gt;
&lt;li&gt;Rename &lt;code dir=&quot;auto&quot;&gt;ReactiveQueryField&lt;/code&gt; usages to &lt;code dir=&quot;auto&quot;&gt;AsyncReactiveField&lt;/code&gt; (codemod).&lt;/li&gt;
&lt;li&gt;Review queries that intentionally rely on multiple collection &lt;code dir=&quot;auto&quot;&gt;LEFT JOIN&lt;/code&gt;s; most should be faster automatically, but unusual cases may need &lt;code dir=&quot;auto&quot;&gt;allowMultipleLeftJoins: true&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;optimizeJoinsToExists: false&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thanks to everyone filing issues, testing &lt;code dir=&quot;auto&quot;&gt;next&lt;/code&gt; releases, and pushing on Joist’s edge cases.&lt;/p&gt;
</content:encoded></item><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><description>Joist uses the unnest function, and our own unnest_arrays function, for bulk updates.</description><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;/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;/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;/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;/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;/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;/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;/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;/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;/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;/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><description>Joist&apos;s plugin infrastructure allowed shipping a non-trivial versioning as a &quot;cross-cutting concern&quot; with a minimum amount of boilerplate.</description><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;/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;/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;/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;/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;/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;/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;/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;/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;/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;/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;/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;/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;/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;/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;/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;/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;/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;/figure&gt;&lt;/div&gt;
&lt;p&gt;Because &lt;code dir=&quot;auto&quot;&gt;get books()&lt;/code&gt; is a method in a JavaScript &lt;code dir=&quot;auto&quot;&gt;class&lt;/code&gt;, the &lt;code dir=&quot;auto&quot;&gt;books&lt;/code&gt; function is installed just once on the &lt;code dir=&quot;auto&quot;&gt;Author.prototype&lt;/code&gt;, not each individual &lt;code dir=&quot;auto&quot;&gt;author&lt;/code&gt; instance, and so when we create &lt;code dir=&quot;auto&quot;&gt;new Author&lt;/code&gt; the only field to initialize is a single shared &lt;code dir=&quot;auto&quot;&gt;#relations&lt;/code&gt; cache, and there “nothing to do” for &lt;code dir=&quot;auto&quot;&gt;books&lt;/code&gt;–it doesn’t need an allocation, it’s just waiting to be called, on the prototype–it’s lazy.&lt;/p&gt;
&lt;p&gt;This works great! We get lazy initialization, &lt;code dir=&quot;auto&quot;&gt;hasMany(&quot;books&quot;)&lt;/code&gt; is only invoked when &lt;code dir=&quot;auto&quot;&gt;books&lt;/code&gt; is accessed, and no one cares that the generated output is a little longer/more verbose.&lt;/p&gt;
&lt;p&gt;However, Joist also has a robust set of user-defined relations, for &lt;a href=&quot;https://joist-orm.io/modeling/why-entities/&quot;&gt;rich domain modeling&lt;/a&gt;, i.e. things like &lt;a href=&quot;https://joist-orm.io/modeling/reactive-fields/&quot;&gt;ReactiveField&lt;/a&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;// 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;&lt;/span&gt;&lt;span&gt;numberOfBooks &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hasReactiveField&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;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;And because this line is fundamentally a user-/engineer-written relation, we couldn’t apply our usual codegen hammer.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;second-approach-ast-transformers&quot;&gt;Second Approach: AST Transformers&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Given we want “this field…but rewritten into a getter”, that sounded like a good job for AST rewriting!&lt;/p&gt;
&lt;p&gt;So our next approach was using &lt;a href=&quot;https://github.com/nonara/ts-patch&quot;&gt;ts-patch&lt;/a&gt; to run a custom transform during the &lt;code dir=&quot;auto&quot;&gt;tsc&lt;/code&gt; compilation process that scanned the AST for class fields following our &lt;code dir=&quot;auto&quot;&gt;has...&lt;/code&gt; convention, and rewrote them to getters.&lt;/p&gt;
&lt;p&gt;So the source &lt;code dir=&quot;auto&quot;&gt;Author.ts&lt;/code&gt; file would have the &lt;code dir=&quot;auto&quot;&gt;books = hasBooks&lt;/code&gt; field syntax, but the &lt;code dir=&quot;auto&quot;&gt;tsc&lt;/code&gt; emitted &lt;code dir=&quot;auto&quot;&gt;Author.js&lt;/code&gt; file would have a &lt;code dir=&quot;auto&quot;&gt;get books() { ... }&lt;/code&gt; getter, very similar to the &lt;code dir=&quot;auto&quot;&gt;AuthorCodegen&lt;/code&gt; getters from the previous approach.&lt;/p&gt;
&lt;p&gt;This was pretty good!&lt;/p&gt;
&lt;p&gt;The pros/cons were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pro: We achieved lazy initialization :tada:&lt;/li&gt;
&lt;li&gt;Pro: Engineers kept writing the ergonomic &lt;code dir=&quot;auto&quot;&gt;relation = hasSome(...)&lt;/code&gt; field syntax&lt;/li&gt;
&lt;li&gt;Pro: No runtime overhead&lt;/li&gt;
&lt;li&gt;Con: Potentially different test vs. production behavior, if tests did not use &lt;code dir=&quot;auto&quot;&gt;ts-jest&lt;/code&gt; &amp;#x26; invoke the transformer&lt;/li&gt;
&lt;li&gt;Con: We’re tightly coupled to &lt;code dir=&quot;auto&quot;&gt;ts-patch&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;tsc&lt;/code&gt;-driven output&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This last con has been becoming more painful, as more alterative performance-improving solutions to “transpile/load TypeScript” have been popping up.&lt;/p&gt;
&lt;p&gt;We’re fans of tsx, Bun, and Node’s built-in TypeScript support–all of which are amazingly fast, but all have spotty or missing transform support (likely on purpose, b/c transforms themselves would only slow things down), and so we’ve not been able to leverage these newer tools.&lt;/p&gt;
&lt;p&gt;This had been fine, but as our majestic monolith codebase grows (and grows), we’re more &amp;#x26; more motivated to switch to a faster build/load process–without giving up the lazy fields performance optimization.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;final-approach-javascript-prototypes&quot;&gt;Final Approach: JavaScript Prototypes&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;We spent quite awhile brainstorming how to “keep fields…but not have them initialized”–literally months of “here &amp;#x26; there” thinking about it, since this was not our top priority, but always an itch, lurking, tingling, just waiting to be scratched.&lt;/p&gt;
&lt;p&gt;We were willing to use proxies, or subtypes, or really any magic–but could not think of a way to avoid the &lt;code dir=&quot;auto&quot;&gt;hasMany&lt;/code&gt; function in &lt;code dir=&quot;auto&quot;&gt;books = hasMany&lt;/code&gt; from running when the constructor is called.&lt;/p&gt;
&lt;p&gt;Stepping back, we established that, tactically, we need to achieve two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create an instance without the fields&lt;/li&gt;
&lt;li&gt;Move the relations onto the prototype&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When articulated this way, a native-JavaScript solution starts to emerge, and even become obvious in retrospect.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;skipping-field-initializers-with-objectcreate&quot;&gt;Skipping field initializers with Object.create&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The insight to avoid field initializers is they happen “when the constructor is called”–what if we just don’t call the constructor? I.e. don’t call &lt;code dir=&quot;auto&quot;&gt;new Author&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;Ideally we’d like to create an empty &lt;code dir=&quot;auto&quot;&gt;author&lt;/code&gt; instance (i.e. with no fields assigned, so no &lt;code dir=&quot;auto&quot;&gt;hasMany&lt;/code&gt; functions invoked), but hooked up to the &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; class’s prototype, so it would still “quack like an Author”.&lt;/p&gt;
&lt;p&gt;Turns out in JavaScript this is a one-liner:&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;empty&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;create&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;prototype&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;And it has all the behaviors we’re looking for:&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;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(empty &lt;/span&gt;&lt;span&gt;instanceof&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;// true!&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;(empty&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;someGetterOnPrototype&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// works&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;(empty&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;someClassField&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// undefined but expected&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Granted, “don’t use &lt;code dir=&quot;auto&quot;&gt;new Author&lt;/code&gt;” sounds like a weird approach, but serendipitously Joist already requires all entity creation to go through the &lt;code dir=&quot;auto&quot;&gt;EntityManager&lt;/code&gt;, i.e.:&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;// Creating new authors must use em.create&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;newAuthor&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;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Loading existing authors must use em.load/em.find&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;oldAuthor&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;So our codebases were &lt;em&gt;already decoupled from the &lt;code dir=&quot;auto&quot;&gt;new&lt;/code&gt; operator&lt;/em&gt;, and using &lt;code dir=&quot;auto&quot;&gt;em.create&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;em.load&lt;/code&gt; instead, as Joist uses these &lt;code dir=&quot;auto&quot;&gt;em.create&lt;/code&gt; &amp;#x26; &lt;code dir=&quot;auto&quot;&gt;em.load&lt;/code&gt; methods to precisely control (and optimize) the entity creation/lifecycle.&lt;/p&gt;
&lt;p&gt;This was lucky! Our &lt;code dir=&quot;auto&quot;&gt;em.create&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;em.load&lt;/code&gt; API meant we already had a single choke point to swap out an optimized &lt;code dir=&quot;auto&quot;&gt;Object.create&lt;/code&gt;-based instantiation flow (instead of using &lt;code dir=&quot;auto&quot;&gt;new&lt;/code&gt;), and have the entire codebase benefit, with very few/ideally zero changes.&lt;/p&gt;
&lt;p&gt;So far this is almost too easy–we’ve got an empty &lt;code dir=&quot;auto&quot;&gt;author&lt;/code&gt;, but of course &lt;code dir=&quot;auto&quot;&gt;author.books.load()&lt;/code&gt; does not work yet (it was a field that we skipped), so how do we get that &lt;code dir=&quot;auto&quot;&gt;books&lt;/code&gt; relation, and all the other relations, back?&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;moving-relations-to-the-prototype&quot;&gt;Moving Relations to the Prototype&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;We need a caller accessing &lt;code dir=&quot;auto&quot;&gt;author.books&lt;/code&gt; to still work, and ideally “find books” on the &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt;’s prototype, which means something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The JavaScript runtime looks for a &lt;code dir=&quot;auto&quot;&gt;&quot;books&quot;&lt;/code&gt; key defined directly on the &lt;code dir=&quot;auto&quot;&gt;author&lt;/code&gt; instance, but does not find one (because we skipped the &lt;code dir=&quot;auto&quot;&gt;new Author&lt;/code&gt; constructor)&lt;/li&gt;
&lt;li&gt;The JavaScript runtime looks up the prototype chain, for a &lt;code dir=&quot;auto&quot;&gt;&quot;books&quot;&lt;/code&gt; key, and finds &lt;code dir=&quot;auto&quot;&gt;Author.prototype[&quot;books&quot;]&lt;/code&gt;, and invokes that, with &lt;code dir=&quot;auto&quot;&gt;author&lt;/code&gt; bound to &lt;code dir=&quot;auto&quot;&gt;this&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This flow would allow us to define &lt;code dir=&quot;auto&quot;&gt;books&lt;/code&gt; “just once” on the &lt;code dir=&quot;auto&quot;&gt;Author.prototype&lt;/code&gt; and have all author instances “get the &lt;code dir=&quot;auto&quot;&gt;books&lt;/code&gt; key” basically for free: they would inherit the &lt;code dir=&quot;auto&quot;&gt;&quot;books&quot;&lt;/code&gt; key from their prototype, without any extra per-&lt;code dir=&quot;auto&quot;&gt;author&lt;/code&gt; instantiation costs.&lt;/p&gt;
&lt;p&gt;Granted, different authors can have different books, so we don’t want &lt;code dir=&quot;auto&quot;&gt;Author.prototype.books&lt;/code&gt; to be a literal &lt;code dir=&quot;auto&quot;&gt;Book[]&lt;/code&gt; array of books–but we could define it as a function that, when called with &lt;code dir=&quot;auto&quot;&gt;this=author1&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;this=author2&lt;/code&gt;, returns the books for that specific author.&lt;/p&gt;
&lt;p&gt;…which is essentially a getter (i.e. we’ve just described how getter methods in JavaScript classes work: they are just syntax sugar for “put this function on the prototype”).&lt;/p&gt;
&lt;p&gt;So, how do we get this &lt;code dir=&quot;auto&quot;&gt;Author.prototype.books&lt;/code&gt; getter defined?&lt;/p&gt;
&lt;p&gt;Because everything in JavaScript is “just an object”, including prototypes, we can dynamically define keys on the prototype with just an &lt;code dir=&quot;auto&quot;&gt;Object.defineProperty&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;Object&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;defineProperty&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;prototype&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;configurable: &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;get&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;any&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;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;hasBooks&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;So easy!&lt;/p&gt;
&lt;p&gt;We just need to call this &lt;code dir=&quot;auto&quot;&gt;defineProperty&lt;/code&gt; for &lt;code dir=&quot;auto&quot;&gt;author.books&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;author.publisher&lt;/code&gt;, and all other relations on &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt;–but how do we know what those relations are?&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;probing-for-relation-metadata&quot;&gt;Probing for Relation Metadata&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;We want to figure out “what are the fields on &lt;code dir=&quot;auto&quot;&gt;Author&lt;/code&gt; that we should move to the prototype”? Ideally just at runtime, i.e. without any AST parsing.&lt;/p&gt;
&lt;p&gt;Who can tell us what the fields are? …the constructor!&lt;/p&gt;
&lt;p&gt;This is ironic, because we’ve been trying so hard to avoid the constructor (avoid calling &lt;code dir=&quot;auto&quot;&gt;new&lt;/code&gt;)–but if we just call it once, during boot, we can use it as a one-time “probe” to discover the fields, and then never call it again.&lt;/p&gt;
&lt;p&gt;We end up with a process like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When Joist boots, create a single “fake” &lt;code dir=&quot;auto&quot;&gt;new Author&lt;/code&gt; to let the constructor assign fields like &lt;code dir=&quot;auto&quot;&gt;books = hasMany(...)&lt;/code&gt; during the traditional object instantiation process&lt;/li&gt;
&lt;li&gt;Teach &lt;code dir=&quot;auto&quot;&gt;hasBooks(...)&lt;/code&gt; to return a &lt;code dir=&quot;auto&quot;&gt;LazyField&lt;/code&gt; marker/wrapper that isn’t the true relation (it cannot actually hold/fetch &lt;code dir=&quot;auto&quot;&gt;Book&lt;/code&gt;s), but instead a lazy version that lets us:
&lt;ul&gt;
&lt;li&gt;a) identify “this &lt;code dir=&quot;auto&quot;&gt;author.books&lt;/code&gt; value is a lazy field”, and&lt;/li&gt;
&lt;li&gt;b) later ask it to create the live relation, only when needed/accessed.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;For every &lt;code dir=&quot;auto&quot;&gt;Object.entries(author)&lt;/code&gt; where &lt;code dir=&quot;auto&quot;&gt;value intanceof LazyField&lt;/code&gt;, do an &lt;code dir=&quot;auto&quot;&gt;Object.defineProperties&lt;/code&gt; to “move” that field to the prototype, and call &lt;code dir=&quot;auto&quot;&gt;lazyField.create()&lt;/code&gt; when lazily accessed:&lt;/li&gt;
&lt;/ol&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;Object&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;defineProperty&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;prototype&lt;/span&gt;&lt;span&gt;, lazyField&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fieldName&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;configurable: &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;get&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;any&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;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;__fields&lt;/span&gt;&lt;span&gt;[lazyField&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fieldName&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;??=&lt;/span&gt;&lt;span&gt; lazyField&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&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;/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;/figure&gt;&lt;/div&gt;
&lt;p&gt;And that’s it–we’ve done a one-time/on-boot “probe” of &lt;code dir=&quot;auto&quot;&gt;new Author&lt;/code&gt;, to find its fields/relations, moved them to the prototype, and now going forward we can do our constructor-avoiding &lt;code dir=&quot;auto&quot;&gt;Object.create(Author.prototype)&lt;/code&gt; call.&lt;/p&gt;
&lt;p&gt;When callers access the relations like &lt;code dir=&quot;auto&quot;&gt;author.books&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;author.publisher&lt;/code&gt; they will still be there, and still “look like fields” to the type system, but they’ll be lazily created.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;non-relation-transient-fields&quot;&gt;Non-Relation Transient Fields&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The approach so far works great for “class fields that are relations”, like &lt;code dir=&quot;auto&quot;&gt;hasMany&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;hasReactiveField&lt;/code&gt;, but what about “just regular fields”, like if you want to have some extra/random class field on an Author?&lt;/p&gt;
&lt;p&gt;Joist already had an established convention for these “not actually persisted but sometimes useful fields”, which was wrapping them in a &lt;code dir=&quot;auto&quot;&gt;transientFields&lt;/code&gt; object:&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;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;&lt;/span&gt;&lt;span&gt;transientFields &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;someSpecialFlag: &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;&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;// use the transient field in some 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;someLogic&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;boolean&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;transientFields&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;someSpecialFlag&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;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;/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;/figure&gt;&lt;/div&gt;
&lt;p&gt;We created this convention solely for developer ergonomics: we want it to be abundantly clear that &lt;code dir=&quot;auto&quot;&gt;someSpecialFlag&lt;/code&gt; is not a database column, and wrapping these one-off fields in a &lt;code dir=&quot;auto&quot;&gt;transientFields&lt;/code&gt; object has been a great way to communicate that.&lt;/p&gt;
&lt;p&gt;Here again we got lucky–because &lt;code dir=&quot;auto&quot;&gt;transientFields&lt;/code&gt; is a known convention/hardcoded name, even though we’re skipping the &lt;code dir=&quot;auto&quot;&gt;new&lt;/code&gt; operator (which normally defines the &lt;code dir=&quot;auto&quot;&gt;transientFields&lt;/code&gt; POJO on each instance), we can apply the same “move to the prototype” trick.&lt;/p&gt;
&lt;p&gt;We &lt;code dir=&quot;auto&quot;&gt;defineProperty&lt;/code&gt; a &lt;code dir=&quot;auto&quot;&gt;Author.prototype.transientFields&lt;/code&gt; getter to lazily create each instances’ &lt;code dir=&quot;auto&quot;&gt;transientFields&lt;/code&gt; POJO on first access:&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;Object&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;defineProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;cstr&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;prototype&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;transientFields&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;get&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;any&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;// Give each instance its own lazily-created copy of transientFields&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;copy&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;structuredClone&lt;/span&gt;&lt;span&gt;(value);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Once defined on the instance, this prototype getter won&apos;t be hit&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// again, b/c author.transientFields will immediately find its instance&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// level copy&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;Object&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;defineProperty&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;&quot;&lt;/span&gt;&lt;span&gt;transientFields&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, { value: copy });&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; copy;&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;So everything still works!&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;performance-achieved&quot;&gt;Performance Achieved&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;So, why do all this?&lt;/p&gt;
&lt;p&gt;We recorded benchmarks from our internal codebase, creating one of our core entities, which has 30+ relations, across these 3 approaches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The vanilla &lt;code dir=&quot;auto&quot;&gt;new cstr()&lt;/code&gt; with no field optimizations at all&lt;/li&gt;
&lt;li&gt;Our previous &lt;code dir=&quot;auto&quot;&gt;joist-transform-properties&lt;/code&gt; ts-patch approach&lt;/li&gt;
&lt;li&gt;The new &lt;code dir=&quot;auto&quot;&gt;Object.create&lt;/code&gt; + prototype approach&lt;/li&gt;
&lt;/ul&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;benchmark                            avg (min … max) p75 / p99    (min … top 1%)&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;new cstr() vanilla js          4.98 µs/iter   5.00 µs   █▅&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;(4.11 µs … 176.31 µs)   8.92 µs   ██▃&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;( 32.00  b …   1.16 mb)  29.87 kb ▁████▅▃▂▂▁▁▁▁▁▁▁▁▁▁▁▁&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;joist-transform-properties   258.26 ns/iter 258.62 ns      █&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;(248.18 ns … 331.49 ns) 279.27 ns      ██&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;(296.04  b … 661.47  b) 496.61  b ▂▁▂▂████▃▄▃▂▂▂▂▁▁▁▁▁▁&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;Object.create()              157.89 ns/iter 156.26 ns ██&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;(152.97 ns … 305.13 ns) 222.49 ns ██&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;(351.71  b … 708.99  b) 441.30  b ██▃▁▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Note that the 1st results 4.98µs is microseconds, while the 2nd and 3rd are nanoseconds, so after normalizing &amp;#x26; comparing them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Our new &lt;code dir=&quot;auto&quot;&gt;Object.create&lt;/code&gt; is 30x faster that vanilla &lt;code dir=&quot;auto&quot;&gt;new Author&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The new &lt;code dir=&quot;auto&quot;&gt;Object.create&lt;/code&gt; is also 1.6x faster than our previous AST-transform approach (likely b/c &lt;code dir=&quot;auto&quot;&gt;transientFields&lt;/code&gt; is lazy now too)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Perhaps more importantly, for memory usage:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The new &lt;code dir=&quot;auto&quot;&gt;Object.create&lt;/code&gt; uses 70x less memory than vanilla &lt;code dir=&quot;auto&quot;&gt;new Author&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The new &lt;code dir=&quot;auto&quot;&gt;Object.create&lt;/code&gt; uses basically the same memory as the AST approach (expected)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We’re really happy with these results!&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;If you want to explore this &lt;code dir=&quot;auto&quot;&gt;Object.create&lt;/code&gt; approach in a minimal example, there is a proof-of-concept repository we used to prototype the approach &lt;a href=&quot;https://github.com/stephenh/lazy-ts-fields/tree/main/src&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/div&gt;&lt;/aside&gt;
&lt;div&gt;&lt;h2 id=&quot;onward&quot;&gt;Onward&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Looking at what we’ve achieved, we’ve made all Joist relations/class fields lazy, with no decorators like &lt;code dir=&quot;auto&quot;&gt;@lazy&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;@ManyToOne&lt;/code&gt;, or our prior AST transforms; instead, just by taking the time to study the problem, and then leaning into one of JavaScript’s core native features–prototypes!&lt;/p&gt;
&lt;p&gt;This work unblocks our current priority: improving the DX of our large, 500-table majestic monolith codebase, to feel as light &amp;#x26; nimble as smaller codebases–and doing so without regressing real-world application performance.&lt;/p&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;/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;/figure&gt;&lt;/div&gt;
&lt;p&gt;Or Java’s Hibernate:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Author.java&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;span&gt;Entity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Table&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;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;&quot;&lt;/span&gt;&lt;span&gt;users&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;public&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;&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;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;GeneratedValue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;strategy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GenerationType&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;IDENTITY&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;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&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;Column&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;unique&lt;/span&gt;&lt;span&gt; &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;span&gt;nullable&lt;/span&gt;&lt;span&gt; &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;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&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;email&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;Column&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;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;&quot;&lt;/span&gt;&lt;span&gt;first_name&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;nullable&lt;/span&gt;&lt;span&gt; &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;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&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;firstName&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;Column&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;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;&quot;&lt;/span&gt;&lt;span&gt;last_name&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;nullable&lt;/span&gt;&lt;span&gt; &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;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&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;lastName&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;OneToMany&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;mappedBy&lt;/span&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;user&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;cascade&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CascadeType&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ALL&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;fetch&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FetchType&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;LAZY&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;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;List&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;orders&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;Joist &lt;strong&gt;avoids decorators&lt;/strong&gt;, and instead uses a &lt;strong&gt;schema-first approach&lt;/strong&gt;, of reading, and code generating, the domain model from the database schema.&lt;/p&gt;
&lt;p&gt;This results in pleasantly succinct entity files that are reminiscent of Rails ActiveRecord:&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;User&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;UserCodegen&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;But there are a few more reasons than just succinctness for this approach.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;typescript-decorators-are-painful&quot;&gt;TypeScript Decorators are Painful&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Because TypeScript decorators shipped years before the official JavaScript decorators, they are clunky to use, as they require build-time infra to bake their metadata into production builds/artifacts.&lt;/p&gt;
&lt;p&gt;When using &lt;code dir=&quot;auto&quot;&gt;tsc&lt;/code&gt;, this is as simple as turning on &lt;code dir=&quot;auto&quot;&gt;emitDecoratorMetadata&lt;/code&gt;, but then projects are limited to &lt;code dir=&quot;auto&quot;&gt;tsc&lt;/code&gt;-derived tooling like &lt;code dir=&quot;auto&quot;&gt;ts-node&lt;/code&gt;, instead of more modern tooling like &lt;a href=&quot;https://github.com/privatenumber/tsx&quot;&gt;tsx&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Admittedly, this should get better with official JavaScript decorators now shipped, but ORMs will have to migrate over to the new standard, which has slightly different (and less powerful, afaiu) semantics.&lt;/p&gt;
&lt;p&gt;For Joist, instead of reading/encoding metadata from &lt;code dir=&quot;auto&quot;&gt;@Column&lt;/code&gt; decorators, Joist’s &lt;code dir=&quot;auto&quot;&gt;joist-codegen&lt;/code&gt; looks at the database directly, and then generates a &lt;code dir=&quot;auto&quot;&gt;metadata.ts&lt;/code&gt; file that is imported on boot, and “just works” (this is similar to Mikro’s &lt;a href=&quot;https://mikro-orm.io/docs/metadata-providers#using-entityschema&quot;&gt;EntitySchema&lt;/a&gt; approach, except that Joist &lt;em&gt;always&lt;/em&gt; uses this approach, and makes it dead simple, instead of being something users have to fiddle with.)&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;decorators-are-not-dry&quot;&gt;Decorators are not DRY&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;For simple primitive fields, 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;  &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;: number;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;@PrimaryKey&lt;/code&gt; line is just fine, but once decorators get more complex, they can become repetitive, i.e. for something like Hibernate’s &lt;code dir=&quot;auto&quot;&gt;OneToMany&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;&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;(&lt;/span&gt;&lt;span&gt;mappedBy&lt;/span&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;user&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;cascade&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CascadeType&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ALL&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;fetch&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FetchType&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;LAZY&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;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;List&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;orders&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The decorator arguments can get more &amp;#x26; more complex, which is bad because it means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The engineer writing &lt;code dir=&quot;auto&quot;&gt;orders&lt;/code&gt; has to repeat “we want our m2o relations to behave like (…the typical arguments we’ve decided to use…)” across the codebase, or&lt;/li&gt;
&lt;li&gt;The engineer using &lt;code dir=&quot;auto&quot;&gt;orders&lt;/code&gt; has to wonder “how was this m2o relation configured? is it like all of our others, or different?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead, Joist uses a declarative/automated approach (code generation): the “rules” or “output” for &lt;code dir=&quot;auto&quot;&gt;varchar&lt;/code&gt; columns is always the same, the output for &lt;code dir=&quot;auto&quot;&gt;timestamptz&lt;/code&gt; columns is always the same, for FK columns, etc.&lt;/p&gt;
&lt;p&gt;Joist’s approach drives consistency across the codebase: if we have 5 foreign key columns, or 100 foreign key columns, or 500 &lt;code dir=&quot;auto&quot;&gt;timestamptz&lt;/code&gt; columns (across our entire schema), they will all act “the same way”.&lt;/p&gt;
&lt;p&gt;This is especially important in large codebases, where consistency is key to maintainability and readability (avoiding violating the Principle of Least Surprise).&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;customization-via-rules&quot;&gt;Customization via Rules&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If applications need different output than Joist’s default output (i.e. handling &lt;code dir=&quot;auto&quot;&gt;timestamptz&lt;/code&gt; columns differently), Joist’s preference is to encode these as config-file rules/flags that are applied &lt;em&gt;across the codebase&lt;/em&gt; to a pattern of columns, instead of repeatively configured/decided on a column-by-column basis.&lt;/p&gt;
&lt;p&gt;A great example of this is our &lt;a href=&quot;https://joist-orm.io/getting-started/configuration/#temporal&quot;&gt;temporal&lt;/a&gt; config flag, which flips all date columns from being mapped as the built-in JavaScript &lt;code dir=&quot;auto&quot;&gt;Date&lt;/code&gt; type to &lt;code dir=&quot;auto&quot;&gt;Temporal&lt;/code&gt;-based types, which are much more ergonomic &amp;#x26; correct to work with.&lt;/p&gt;
&lt;p&gt;Other than &lt;code dir=&quot;auto&quot;&gt;temporal&lt;/code&gt;, we don’t have many other customizations available, primarily because we’ve not needed them yet–if you do, feel free to open an issue on GitHub!&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;schema-should-be-source-of-truth&quot;&gt;Schema Should be Source of Truth&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Joist’s view is that, once your application is in production, writing “diff-based” migrations (i.e. &lt;a href=&quot;https://github.com/salsita/node-pg-migrate/&quot;&gt;node-pg-migrate&lt;/a&gt;) is the better than “code-first” schema management.&lt;/p&gt;
&lt;p&gt;This is because the diff-based migrations are heavily grounded in “what is the production data &lt;em&gt;now&lt;/em&gt;”, vs. code-first migrations (from decorators or Prisma’s domain model file), which make it almost “too easy” to make large, sweeping domain model changes, without engineer’s really realizing (or at least deferring) how the production data will be brought along to the new world.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;using-joist-configjson&quot;&gt;Using &lt;code dir=&quot;auto&quot;&gt;joist-config.json&lt;/code&gt;&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Joist is able to get ~90-95% of metadata it needs directly from the database, but there is always that last 5% of config that is not available in the database itself–things like renaming “other side” relations, STI inheritance behavior, and a few other things.&lt;/p&gt;
&lt;p&gt;For these, Joist uses a &lt;code dir=&quot;auto&quot;&gt;joist-config.json&lt;/code&gt; file (see its &lt;a href=&quot;https://joist-orm.io/getting-started/configuration/&quot;&gt;documentation&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;This has been fine, but is still kind of a “least terrible” approach–eventually Joist might push &lt;em&gt;all&lt;/em&gt; config into the database schema (via &lt;code dir=&quot;auto&quot;&gt;COMMENTS&lt;/code&gt; fields, which we use already for renaming FKs columns, or other tricks), or trying a “schema in a DSL” approach, similar to &lt;a href=&quot;https://entgo.io/docs/schema-def&quot;&gt;entgo’s schemadef&lt;/a&gt; (which is admittedly a “code first” approach, but doesn’t intermix the “schema definition” with “entity definition” like decorators do).&lt;/p&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;/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;/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;/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;/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;/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;/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;/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;AsyncReactiveField&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;/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;/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;/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;/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;/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;/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;/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;/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;/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;/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;/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;/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;/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; &amp;#x27;grid&amp;#x27;,=&quot;&quot; placeitems:=&quot;&quot; &amp;#x27;center&amp;#x27;=&quot;&quot; }}=&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&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; &amp;#x27;24px&amp;#x27;=&quot;&quot; }}=&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;/figure&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;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;/figure&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;/li&gt;
&lt;/ul&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;toJSON&lt;/code&gt; implementation will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load any relations that are not yet loaded from the database&lt;/li&gt;
&lt;li&gt;Output only the keys that are requested in the &lt;code dir=&quot;auto&quot;&gt;authorHint&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Call any lambdas like &lt;code dir=&quot;auto&quot;&gt;customField&lt;/code&gt; to generate custom values&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As with previous examples, all data loading is N+1 safe, and also potentially join-based preloaded.&lt;/p&gt;
&lt;p&gt;See the &lt;a href=&quot;https://joist-orm.io/advanced/json-payloads&quot;&gt;toJSON&lt;/a&gt; docs for more information.&lt;/p&gt;
&lt;div&gt;&lt;p&gt;This recursive &lt;code dir=&quot;auto&quot;&gt;toJSON&lt;/code&gt; payload generation is a relatively new feature of Joist, so if you have feature ideas that would make it more useful, please let us know!&lt;/p&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;join-based-preloading&quot;&gt;Join-Based Preloading&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The last optimization that Joist can do is join-based preloading, which can be used in either the RSC or RCC approach.&lt;/p&gt;
&lt;p&gt;This is also a newer feature that requires opt-ing in to, but in &lt;code dir=&quot;auto&quot;&gt;em.ts&lt;/code&gt; you can add a &lt;code dir=&quot;auto&quot;&gt;preloadPlugin&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;/** Returns this request&apos;s `EntityManager` instance. */&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;getEm&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;cache&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;()&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;// Opt-in to preloading&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;const &lt;/span&gt;&lt;span&gt;preloadPlugin&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;JsonAggregatePreloader&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;return &lt;/span&gt;&lt;span&gt;new&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;driver&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;preloadPlugin&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;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This will allow Joist to load a deep tree/subgraph of entities in a single SQL call.&lt;/p&gt;
&lt;p&gt;For example, normally a Joist &lt;code dir=&quot;auto&quot;&gt;em.find&lt;/code&gt; a call 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;const &lt;/span&gt;&lt;span&gt;a&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;find&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;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;{ id: &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;&lt;/span&gt;&lt;span&gt;{populate: { books: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;reviews&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;div&gt;&lt;div&gt;&lt;span&gt;// Now access all the data in memory&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;(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;0&lt;/span&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;get&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;rating&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Will issue three SQL 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;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;=&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;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; books &lt;/span&gt;&lt;span&gt;WHERE&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;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;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; reviews &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; book_id &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;span&gt;2&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;/figure&gt;&lt;/div&gt;
&lt;p&gt;But with the &lt;code dir=&quot;auto&quot;&gt;preloadPlugin&lt;/code&gt; enabled, it will use a single SQL call that uses &lt;code dir=&quot;auto&quot;&gt;CROSS JOIN LATERAL&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;json_agg&lt;/code&gt; to return the author’s books, and the book’s reviews (omitted for brevity) in a single row:&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;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;_b&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; _b &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; authors &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; 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;cross join&lt;/span&gt;&lt;span&gt; lateral&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;-- create a tuple for each book, and aggregate then into an array of 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;select&lt;/span&gt;&lt;span&gt; json_agg(json_build_array(&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;_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;_b&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;foreword&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;order by&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;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;from&lt;/span&gt;&lt;span&gt; books _b&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;_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;&lt;/span&gt;&lt;span&gt;) _b&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;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;limit&lt;/span&gt;&lt;span&gt; ?&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;p&gt;Joist’s join-based preloading is still a beta feature, so if you run into any issues, please let us know!&lt;/p&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;what-about-complex-queries&quot;&gt;What about Complex Queries?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;So far, our queries have focused on loading “just entities”, and then putting those on the wire (or rendering them to HTML).&lt;/p&gt;
&lt;p&gt;This is because Joist’s focus is on building robust domain models, and specifically helping solve the “write-side” of your application’s business logic (running the correct &lt;a href=&quot;https://joist-orm.io/modeling/validation-rules&quot;&gt;validation rules&lt;/a&gt;, &lt;a href=&quot;https://joist-orm.io/modeling/lifecycle-hooks&quot;&gt;lifecycle hooks&lt;/a&gt;, &lt;a href=&quot;https://joist-orm.io/modeling/reactive-fields&quot;&gt;reactive updates&lt;/a&gt;), and less so on the “read-side” of complex queries (i.e. that using aggregates using &lt;code dir=&quot;auto&quot;&gt;GROUP BY&lt;/code&gt;, multiple nested subqueries/projections/etc.).&lt;/p&gt;
&lt;p&gt;As such, Joist does not yet have a sophisticated query builder that can create arbitrary SQL queries, like Kysley or Drizzle.&lt;/p&gt;
&lt;p&gt;Instead, Joist encourages an approach that uses its robust write-side features to create materialized columns in the database, such that the majority of your pages/responses really can be served by “super simple &lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt; statements”, instead of using complicated queries to calculate aggregates on-the-fly.&lt;/p&gt;
&lt;p&gt;Although you can of course use both approaches, and just use a lower-level query builder where needed.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;sample-app-feedback&quot;&gt;Sample App Feedback&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Joist’s roots come from the GraphQL world, so this sample app was our first foray into using it for a NextJS application. If we’ve missed any key features that would make it easier to use Joist in a NextJS app, please let us know!&lt;/p&gt;
</content:encoded></item></channel></rss>