<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://www.yellowduck.be/pretty-atom-feed-v3.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <link rel="alternate" href="https://www.yellowduck.be"/>
  <link rel="self" href="https://www.yellowduck.be/posts/feed"/>
  <author>
    <name>Pieter Claerhout</name>
    <email>pieter@yellowduck.be</email>
  </author>
  <id>https://www.yellowduck.be/posts/feed</id>
  <title>🐥 YellowDuck.be</title>
  <updated>2026-04-02T17:00:00Z</updated>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/installing-claude-code-on-macos-with-homebrew-and-getting-the-latest-version"/>
    <content type="html">&lt;p&gt;If you’re installing Claude Code on macOS using Homebrew, the official instruction currently suggests:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While this works, it installs the &lt;strong&gt;latest stable version&lt;/strong&gt;, not the &lt;strong&gt;latest available version&lt;/strong&gt;. Depending on your use case, that can leave you behind on features and fixes.&lt;/p&gt;
&lt;h1&gt;Install the latest version&lt;/h1&gt;
&lt;p&gt;To install the most recent release, you should explicitly use:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code@latest&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures you’re running the newest version instead of the lagging stable cask.&lt;/p&gt;
&lt;h1&gt;Fix an existing installation&lt;/h1&gt;
&lt;p&gt;If you already installed &lt;code&gt;claude-code&lt;/code&gt; using the default command, you’ll need to replace it:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;uninstall&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code@latest&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That removes the stable version and installs the latest one cleanly.&lt;/p&gt;
&lt;h1&gt;Why this matters&lt;/h1&gt;
&lt;p&gt;Homebrew distinguishes between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stable casks&lt;/strong&gt; → default installs (&lt;code&gt;claude-code&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Versioned or alternative casks&lt;/strong&gt; → explicit installs (&lt;code&gt;claude-code@latest&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this case, the naming is a bit misleading because &lt;code&gt;@latest&lt;/code&gt; is not the default. This has already caused confusion in the community and is being discussed upstream.&lt;/p&gt;
&lt;h1&gt;More context&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Issue discussion: &lt;a href=&quot;https://github.com/anthropics/claude-code/issues/42176&quot;&gt;https://github.com/anthropics/claude-code/issues/42176&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Homebrew PR: &lt;a href=&quot;https://github.com/Homebrew/homebrew-cask/pull/255221&quot;&gt;https://github.com/Homebrew/homebrew-cask/pull/255221&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Takeaway&lt;/h1&gt;
&lt;p&gt;If you want the newest Claude Code features and fixes, don’t rely on the default install. Use:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code@latest&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and you’ll avoid running an outdated version without realizing it.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/announcement&quot;&gt;#announcement&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/mac&quot;&gt;#mac&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-04-02T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/installing-claude-code-on-macos-with-homebrew-and-getting-the-latest-version</id>
    <title>🐥 Installing Claude Code on macOS with Homebrew (and getting the latest version)</title>
    <updated>2026-04-02T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/taming-scrollbars-in-a-phoenix-app"/>
    <content type="html">&lt;p&gt;One of those tiny UI details that quietly annoys users more than you&apos;d expect: scrollbars that flash in and out of existence, causing the page layout to jump around. This week I finally cleaned it up in my Phoenix app and the fix was surprisingly elegant.&lt;/p&gt;
&lt;h1&gt;The problem&lt;/h1&gt;
&lt;p&gt;The original &lt;code&gt;root.html.heex&lt;/code&gt; had this on the &lt;code&gt;&lt;html&gt;&lt;/code&gt; element:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-html&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;en&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;[scrollbar-gutter:stable]&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;scrollbar-gutter: stable&lt;/code&gt; is a CSS property that reserves space for the scrollbar even when it isn&apos;t needed — the idea being to prevent layout shifts when content height changes. It&apos;s a reasonable approach, but it has a side effect: on macOS with &quot;Show scroll bars: Always&quot;, you end up with a permanent empty gutter on pages that don&apos;t scroll. On Windows, where scrollbars are visible by default, the gutter is always there. Depending on your layout, that reserved space can look odd.&lt;/p&gt;
&lt;h1&gt;The solution&lt;/h1&gt;
&lt;p&gt;I removed the Tailwind utility class from the &lt;code&gt;&lt;html&gt;&lt;/code&gt; tag and instead reached for a small CSS snippet:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-css&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;/* Only show scrollbars when content actually overflows */&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;property&quot;&gt;scrollbar-width&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; thin&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;property&quot;&gt;scrollbar-color&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; transparent transparent&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;hover&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;property&quot;&gt;scrollbar-color&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number-float&quot;&gt;0.2&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; transparent&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What this does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;scrollbar-width: thin&lt;/code&gt;&lt;/strong&gt; — uses the browser&apos;s thin scrollbar variant, which is less visually heavy than the default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;scrollbar-color: transparent transparent&lt;/code&gt;&lt;/strong&gt; — hides the scrollbar thumb and track by making them fully transparent. The scrollbar doesn&apos;t disappear from the DOM; it just becomes invisible when you&apos;re not interacting with the element.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;*:hover &amp;lbrace; scrollbar-color: rgba(0,0,0,0.2) transparent &amp;rbrace;&lt;/code&gt;&lt;/strong&gt; — fades the scrollbar thumb in (as a subtle translucent grey) only when the user hovers over the element. This gives a clean overlay-style scrollbar feel, similar to what macOS does natively.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Why this feels better&lt;/h1&gt;
&lt;p&gt;The end result is a UI that looks clean and uncluttered at rest, but still gives users a clear scrollbar affordance the moment they move their cursor over a scrollable area. No layout jump, no reserved gutter space, and no permanently visible chrome competing for attention.&lt;/p&gt;
&lt;p&gt;It&apos;s a two-file change — one CSS block and the removal of a single Tailwind class — but the visual impact is noticeable, especially on pages with sidebars or nested scrollable containers.&lt;/p&gt;
&lt;h1&gt;Browser support&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;scrollbar-width&lt;/code&gt; and &lt;code&gt;scrollbar-color&lt;/code&gt; are part of the &lt;a href=&quot;https://www.w3.org/TR/css-scrollbars-1/&quot;&gt;CSS Scrollbars Specification&lt;/a&gt; and have solid support in Firefox and Chromium-based browsers. Safari added support in version 18.2. For older Safari versions the scrollbar will just render with its default appearance — a perfectly acceptable fallback.&lt;/p&gt;
&lt;p&gt;Small change, cleaner app. Sometimes that&apos;s all it takes.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/css&quot;&gt;#css&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/html&quot;&gt;#html&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-31T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/taming-scrollbars-in-a-phoenix-app</id>
    <title>🐥 Taming scrollbars in a Phoenix app</title>
    <updated>2026-03-31T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/a-better-way-using-mkcert-for-https-in-phoenix-on-macos"/>
    <content type="html">&lt;p&gt;In a previous post, I showed how to use &lt;code&gt;mix phx.gen.cert&lt;/code&gt; to set up HTTPS in Phoenix development. While that approach works in theory, in practice it&apos;s a minefield: OpenSSL 3.x generates PKCS12 bundles that macOS&apos;s &lt;code&gt;security&lt;/code&gt; command rejects, browsers send cryptic &lt;code&gt;Decode Error&lt;/code&gt; alerts, and manually trusting certificates in Keychain Access often has no effect at all.&lt;/p&gt;
&lt;p&gt;There&apos;s a much better tool for this: &lt;a href=&quot;https://github.com/FiloSottile/mkcert&quot;&gt;mkcert&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;What makes mkcert different?&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;mkcert&lt;/code&gt; creates a local Certificate Authority (CA) on your machine and registers it with macOS&apos;s system trust store, Firefox, and Chrome in one command. Any certificate you generate from it is automatically trusted — no manual Keychain fiddling required.&lt;/p&gt;
&lt;h1&gt;Step 1: Install mkcert and register the local CA&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;mkcert&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;mkcert&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-install&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-install&lt;/code&gt; step is what makes everything work. It adds mkcert&apos;s root CA to your system keychain so all browsers trust it going forward.&lt;/p&gt;
&lt;p&gt;Verify it landed:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;security&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;find-certificate&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;mkcert&quot;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Step 2: Generate a certificate for localhost&lt;/h1&gt;
&lt;p&gt;From your Phoenix project root:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;mkcert&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-cert-file&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;priv/cert/selfsigned.pem&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;       &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;-key-file&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;priv/cert/selfsigned_key.pem&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;       &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;localhost&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;127.0.0.1&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;::1&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This generates a certificate valid for &lt;code&gt;localhost&lt;/code&gt;, &lt;code&gt;127.0.0.1&lt;/code&gt;, and &lt;code&gt;::1&lt;/code&gt;, signed by your local CA.&lt;/p&gt;
&lt;h1&gt;Step 3: Configure Phoenix for HTTPS&lt;/h1&gt;
&lt;p&gt;Update &lt;code&gt;config/dev.exs&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:your_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;YourAppWeb.Endpoint&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;https: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;port: &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;4001&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;cipher_suite: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:strong&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;certfile: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;priv/cert/selfsigned.pem&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;keyfile: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;priv/cert/selfsigned_key.pem&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;check_origin: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;code_reloader: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;debug_errors: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start your server:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;phx.server&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Visit &lt;code&gt;https://localhost:4001&lt;/code&gt; — no browser warnings, no certificate errors, no Keychain gymnastics.&lt;/p&gt;
&lt;h1&gt;What about the cert files in version control?&lt;/h1&gt;
&lt;p&gt;The generated &lt;code&gt;priv/cert/&lt;/code&gt; files are already in &lt;code&gt;.gitignore&lt;/code&gt; when using &lt;code&gt;mix phx.gen.cert&lt;/code&gt;, and should stay there with mkcert too. Each developer on your team runs &lt;code&gt;mkcert -install&lt;/code&gt; and generates their own certificate locally.&lt;/p&gt;
&lt;h1&gt;Upgrading from the old approach&lt;/h1&gt;
&lt;p&gt;If you followed the previous post, you can replace the existing cert files in place — the Phoenix config stays the same since you&apos;re still pointing at &lt;code&gt;priv/cert/selfsigned.pem&lt;/code&gt; and &lt;code&gt;priv/cert/selfsigned_key.pem&lt;/code&gt;. Just regenerate them with mkcert and restart your server.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/terminal&quot;&gt;#terminal&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-30T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/a-better-way-using-mkcert-for-https-in-phoenix-on-macos</id>
    <title>🐥 A better way: Using mkcert for HTTPS in Phoenix on macOS</title>
    <updated>2026-03-30T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/fixing-a-race-condition-in-oban-job-counting-with-telemetry"/>
    <content type="html">&lt;p&gt;When building a LiveView dashboard that shows how many background jobs are still processing, a subtle race condition can make the count permanently off by one. Here&apos;s how I ran into it and how Oban&apos;s telemetry system solved it cleanly.&lt;/p&gt;
&lt;h1&gt;The Setup&lt;/h1&gt;
&lt;p&gt;The app has an Oban worker — &lt;code&gt;ProcessExternalLinkWorker&lt;/code&gt; — that fetches a URL, extracts content, and creates a post. The LiveView index page shows a &quot;X post(s) currently being processed&quot; banner while jobs are in flight.&lt;/p&gt;
&lt;p&gt;The job count is a straightforward Oban query:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;where: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;not in&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;completed&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;discarded&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;worker&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;worker&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;aggregate&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:count&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The LiveView subscribes to a &lt;code&gt;&quot;posts&quot;&lt;/code&gt; PubSub topic and refreshes this count whenever a &lt;code&gt;&quot;post_updated&quot;&lt;/code&gt; message arrives. That message is broadcast from inside &lt;code&gt;Posts.create_post/1&lt;/code&gt;, which is called from within the worker.&lt;/p&gt;
&lt;h1&gt;The Bug&lt;/h1&gt;
&lt;p&gt;Here&apos;s the execution sequence that causes the problem:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Oban picks up a job — state transitions to &lt;code&gt;executing&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The worker calls &lt;code&gt;ProcessExternalLink.process_url/1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;That calls &lt;code&gt;Posts.create_post/1&lt;/code&gt;, which broadcasts &lt;code&gt;&quot;post_updated&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The LiveView receives the broadcast and re-queries the job count&lt;/li&gt;
&lt;li&gt;The job is &lt;strong&gt;still &lt;code&gt;executing&lt;/code&gt;&lt;/strong&gt; — it hasn&apos;t returned &lt;code&gt;:ok&lt;/code&gt; yet&lt;/li&gt;
&lt;li&gt;The count includes this job, showing 1 item &quot;still processing&quot; even though the work is done&lt;/li&gt;
&lt;li&gt;The worker returns &lt;code&gt;:ok&lt;/code&gt;, Oban marks the job &lt;code&gt;completed&lt;/code&gt; — but no one tells the LiveView&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The post list refreshes correctly, but the processing counter stays at 1 until the next page load.&lt;/p&gt;
&lt;p&gt;Subtracting 1 from the count isn&apos;t a fix — with multiple concurrent jobs, you&apos;d need to know exactly how many are in this &quot;just finished broadcasting but not yet completed&quot; state, which is unknowable from the outside.&lt;/p&gt;
&lt;h1&gt;The Fix: Oban Telemetry&lt;/h1&gt;
&lt;p&gt;Oban emits telemetry events throughout the job lifecycle. The key one here is &lt;code&gt;[:oban, :job, :stop]&lt;/code&gt;, which fires &lt;strong&gt;after&lt;/strong&gt; the job state has been updated to &lt;code&gt;completed&lt;/code&gt; in the database. There&apos;s also &lt;code&gt;[:oban, :job, :exception]&lt;/code&gt; for failed jobs.&lt;/p&gt;
&lt;p&gt;The fix is to decouple the job count refresh from the &lt;code&gt;&quot;post_updated&quot;&lt;/code&gt; broadcast. Instead, attach a telemetry handler that broadcasts a separate &lt;code&gt;&quot;jobs_updated&quot;&lt;/code&gt; message when a job finishes:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyWebApp.ObanTelemetryHandler&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Logger&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;attach&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;type&quot;&gt;:telemetry&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;detach&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;oban-job-lifecycle&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;type&quot;&gt;:telemetry&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;attach_many&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;oban-job-lifecycle&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:oban&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:job&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:stop&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:oban&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:job&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:exception&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;constant-builtin&quot;&gt;__MODULE__&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function&quot;&gt;handle_event&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;      &lt;span class=&quot;constant-builtin&quot;&gt;nil&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;handle_event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:oban&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:job&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_measurements&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;meta&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_config&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;module&quot;&gt;Logger&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;Oban job &lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;#&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;: worker=&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;#&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;meta&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:worker&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;module&quot;&gt;Phoenix.PubSub&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;broadcast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;MyWebApp.PubSub&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;posts&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;event: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;jobs_updated&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two design decisions worth noting:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;detach&lt;/code&gt; before &lt;code&gt;attach&lt;/code&gt;&lt;/strong&gt;: Calling &lt;code&gt;attach_many&lt;/code&gt; with a handler ID that&apos;s already registered raises an &lt;code&gt;ArgumentError&lt;/code&gt;. In development, a full server restart re-runs &lt;code&gt;application.ex&lt;/code&gt; and would hit this error on the second start. Calling &lt;code&gt;detach&lt;/code&gt; first makes &lt;code&gt;attach/0&lt;/code&gt; idempotent at the cost of one no-op call.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No worker filter&lt;/strong&gt;: An earlier version filtered on the worker name in the handler&apos;s pattern match. That&apos;s redundant — the Ecto query in the LiveView already scopes the count to the specific worker. Removing the filter keeps the handler simpler and avoids fragility around how Oban formats the worker name in telemetry metadata.&lt;/p&gt;
&lt;p&gt;Call &lt;code&gt;attach/0&lt;/code&gt; in &lt;code&gt;application.ex&lt;/code&gt; after the supervisor starts:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Supervisor&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;start_link&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;children&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;module&quot;&gt;MyWebApp.ObanTelemetryHandler&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;attach&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then handle the new event in the LiveView, separate from &lt;code&gt;&quot;post_updated&quot;&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;handle_info&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;event: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;jobs_updated&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;worker&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;MyWebApp.Workers.ProcessExternalLinkWorker&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;num_processing&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;where: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;not in&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;completed&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;discarded&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;worker&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;worker&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;aggregate&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:count&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;assign&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:num_processing&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;num_processing&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Why This Works&lt;/h1&gt;
&lt;p&gt;The &lt;code&gt;&quot;post_updated&quot;&lt;/code&gt; broadcast still fires mid-job and the post list still refreshes correctly — that part was never broken. But the job count is now only refreshed in response to the telemetry event, which is guaranteed to fire after the state change has been committed. The LiveView queries at the right moment.&lt;/p&gt;
&lt;p&gt;It also handles failure correctly. If the worker raises an exception, &lt;code&gt;[:oban, :job, :exception]&lt;/code&gt; fires, the LiveView refreshes the count, and any retryable or discarded jobs show up accurately.&lt;/p&gt;
&lt;h1&gt;Takeaway&lt;/h1&gt;
&lt;p&gt;When displaying counts or status derived from job state, don&apos;t trigger the refresh from within the job itself. The job is still running at that point. Instead, hook into Oban&apos;s telemetry events, which fire at well-defined points in the lifecycle after state transitions have been committed.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-29T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/fixing-a-race-condition-in-oban-job-counting-with-telemetry</id>
    <title>🐥 Fixing a race condition in Oban job counting with telemetry</title>
    <updated>2026-03-29T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/til-filtering-github-prs-that-are-ready-for-review-and-not-yours"/>
    <content type="html">&lt;p&gt;When reviewing pull requests in GitHub, I often want a clean list of PRs that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;are open&lt;/li&gt;
&lt;li&gt;are not drafts&lt;/li&gt;
&lt;li&gt;are not created by me&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the filter I use:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;sort:updated-desc is:pr is:open -author:username draft:false
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;What this does&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sort:updated-desc&lt;/code&gt; → most recently updated PRs first&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is:pr&lt;/code&gt; → only pull requests&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is:open&lt;/code&gt; → only open PRs&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-author:username&lt;/code&gt; → exclude your own PRs&lt;/li&gt;
&lt;li&gt;&lt;code&gt;draft:false&lt;/code&gt; → exclude draft PRs (only show ready-for-review)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gives a focused, high-signal list of PRs that are actually actionable.&lt;/p&gt;
&lt;h1&gt;Bonus: explore more filters&lt;/h1&gt;
&lt;p&gt;GitHub’s search syntax is surprisingly powerful. You can filter by labels, review status, checks, branches, and more.&lt;/p&gt;
&lt;p&gt;Check out the full reference here:
&lt;a href=&quot;https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests&quot;&gt;https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A few useful additions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;review-requested:@me&lt;/code&gt; → PRs requesting your review&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status:success&lt;/code&gt; → only PRs with passing checks&lt;/li&gt;
&lt;li&gt;&lt;code&gt;label:bug&lt;/code&gt; → filter by label&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-is:merged&lt;/code&gt; → exclude merged PRs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you start combining these, you can build very tailored review dashboards directly in GitHub.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/github&quot;&gt;#github&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-28T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/til-filtering-github-prs-that-are-ready-for-review-and-not-yours</id>
    <title>🐥 TIL: filtering GitHub PRs that are ready for review and not yours</title>
    <updated>2026-03-28T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/enforcing-polymorphic-integrity-in-postgresql-with-num-nonnulls"/>
    <content type="html">&lt;p&gt;Polymorphic associations are common when a single table can reference multiple other tables. A typical implementation is one table with multiple nullable foreign keys:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;my_poly_assocs&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;bigserial&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;assoc_a_id&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;bigint&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;REFERENCES&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;assoc_a&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;assoc_b_id&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;bigint&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;REFERENCES&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;assoc_b&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;assoc_c_id&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;bigint&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;REFERENCES&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;assoc_c&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The intent is simple: each row should reference &lt;strong&gt;exactly one&lt;/strong&gt; of these associations. But the database won’t enforce that automatically. Without extra constraints, you can end up with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No association set (all NULL)&lt;/li&gt;
&lt;li&gt;Multiple associations set (invalid state)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PostgreSQL has a clean solution for this.&lt;/p&gt;
&lt;h1&gt;The &lt;code&gt;num_nonnulls&lt;/code&gt; function&lt;/h1&gt;
&lt;p&gt;PostgreSQL provides a built-in function called &lt;code&gt;num_nonnulls&lt;/code&gt;. It returns the number of arguments that are not NULL.&lt;/p&gt;
&lt;p&gt;That makes it perfect for enforcing “exactly one” semantics:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;my_poly_assocs&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;ADD&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;CONSTRAINT&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; exactly_one_assoc_referenced
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;CHECK&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;num_nonnulls&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;assoc_a_id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;assoc_b_id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;assoc_c_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;number-float&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This constraint guarantees:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;At least one foreign key is set&lt;/li&gt;
&lt;li&gt;No more than one foreign key is set&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a row violates the rule, the insert or update fails immediately.&lt;/p&gt;
&lt;h1&gt;Why this is better than application-level checks&lt;/h1&gt;
&lt;p&gt;You could enforce this rule in your application layer, but that leaves room for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Race conditions&lt;/li&gt;
&lt;li&gt;Multiple services writing to the same database&lt;/li&gt;
&lt;li&gt;Future code paths forgetting the rule&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A &lt;code&gt;CHECK&lt;/code&gt; constraint keeps the invariant inside the database, where it belongs.&lt;/p&gt;
&lt;h1&gt;Variations&lt;/h1&gt;
&lt;p&gt;If your requirement is “at most one” instead of “exactly one”, you can adjust the constraint:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;text&quot;&gt;CHECK &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;num_nonnulls&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;assoc_a_id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; assoc_b_id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; assoc_c_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;=&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; 1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you later add a new polymorphic target, you must update the constraint to include the new column.&lt;/p&gt;
&lt;h1&gt;When to use this pattern&lt;/h1&gt;
&lt;p&gt;This approach works well when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need strict relational integrity.&lt;/li&gt;
&lt;li&gt;The set of polymorphic targets is finite and known.&lt;/li&gt;
&lt;li&gt;You want predictable query performance without an additional type column.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your targets are dynamic or numerous, a more classic polymorphic design (e.g. &lt;code&gt;target_type&lt;/code&gt; + &lt;code&gt;target_id&lt;/code&gt;) may be more flexible, though it trades off referential integrity.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;num_nonnulls&lt;/code&gt; is a small but powerful feature in PostgreSQL. It allows you to enforce a subtle but important invariant with a single CHECK constraint, keeping your polymorphic associations consistent and safe at the database level.&lt;/p&gt;
&lt;p&gt;It’s one of those features that feels obvious once you know it exists.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/postgresql&quot;&gt;#postgresql&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sql&quot;&gt;#sql&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-27T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/enforcing-polymorphic-integrity-in-postgresql-with-num-nonnulls</id>
    <title>🐥 Enforcing polymorphic integrity in PostgreSQL with num_nonnulls</title>
    <updated>2026-03-27T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/request-validation-in-phoenix-the-laravel-formrequest-approach"/>
    <content type="html">&lt;p&gt;Developers coming from Laravel are used to &lt;strong&gt;FormRequest classes&lt;/strong&gt; that encapsulate request validation and authorization. A typical FormRequest contains validation rules, optional authorization logic, and automatically provides validated input to the controller.&lt;/p&gt;
&lt;p&gt;Phoenix takes a slightly different approach. Instead of request-focused validation objects, validation is typically handled using &lt;strong&gt;Ecto changesets&lt;/strong&gt;. This approach moves validation closer to the data model and keeps controllers thin.&lt;/p&gt;
&lt;p&gt;This article explains how validation works in Phoenix and how to implement reusable custom validation rules similar to Laravel.&lt;/p&gt;
&lt;h2&gt;Validation with Ecto changesets&lt;/h2&gt;
&lt;p&gt;In Phoenix, validation is usually implemented inside an Ecto changeset. A changeset handles three responsibilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;casting incoming parameters&lt;/li&gt;
&lt;li&gt;validating data&lt;/li&gt;
&lt;li&gt;collecting validation errors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A typical schema with validations looks like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Accounts.User&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Ecto.Schema&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Ecto.Changeset&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;schema&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;users&quot;&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;field&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:string&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;field&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:string&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_required&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_format&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;r/@/&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Incoming request data is passed to the changeset through a context function:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;create_user&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The controller then handles the result:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Accounts&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;create_user&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;      &lt;span class=&quot;function-call&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;put_status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:unprocessable_entity&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;errors: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This already provides most of the functionality developers expect from Laravel FormRequests.&lt;/p&gt;
&lt;h2&gt;Writing custom validation rules&lt;/h2&gt;
&lt;p&gt;Custom validation logic in Phoenix is implemented as functions that operate on a changeset. These functions can be private helpers or reusable validation utilities.&lt;/p&gt;
&lt;h3&gt;Field-level custom validation&lt;/h3&gt;
&lt;p&gt;The most common tool for custom rules is &lt;code&gt;validate_change/3&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_company_email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;validate_change&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;email&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;ends_with?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;@company.com&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;email: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;must be a company email&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can include this in a changeset pipeline:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_required&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_company_email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the validation fails, an error is added to the changeset.&lt;/p&gt;
&lt;h3&gt;Reusable validation helpers&lt;/h3&gt;
&lt;p&gt;If validation logic should be reused across schemas, it can be extracted into a module.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Validations&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Ecto.Changeset&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_company_email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;validate_change&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;email&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;ends_with?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;@company.com&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;must be a company email&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;      &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Usage inside a changeset:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Validations&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;user&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_required&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_company_email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern is similar to reusable validation rules in Laravel.&lt;/p&gt;
&lt;h3&gt;Cross-field validation&lt;/h3&gt;
&lt;p&gt;Some rules depend on multiple fields. In these cases, the changeset can be inspected directly.&lt;/p&gt;
&lt;p&gt;For example, validating a date range:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_date_range&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;get_field&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;get_field&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;compare&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:gt&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;add_error&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;must be before end date&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;changeset&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Used inside a changeset:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;event&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_required&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;validate_date_range&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Database-backed validation&lt;/h3&gt;
&lt;p&gt;Some validation rules depend on the database. For example, checking whether an email already exists.&lt;/p&gt;
&lt;p&gt;While this can be implemented manually, the preferred approach is to rely on database constraints.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;unique_constraint&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This requires a unique index in the database and prevents race conditions that can occur with manual checks.&lt;/p&gt;
&lt;h2&gt;Key building blocks&lt;/h2&gt;
&lt;p&gt;Custom validation in Ecto is built on a few core functions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;validate_change/3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add_error/3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_field/2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate_required/2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate_length/3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate_format/3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate_number/3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate_inclusion/3&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most complex validation logic can be composed from these primitives.&lt;/p&gt;
&lt;h2&gt;Comparing Laravel and Phoenix validation&lt;/h2&gt;
&lt;p&gt;Laravel focuses validation around the HTTP request, while Phoenix places validation closer to the data layer.&lt;/p&gt;
&lt;p&gt;Laravel:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;FormRequest&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  ↓
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;V&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;alidator&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  ↓
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;ontroller&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Phoenix:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;Controller&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  ↓
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;ontext&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  ↓
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;hangeset&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This design makes validation reusable across:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP APIs&lt;/li&gt;
&lt;li&gt;Phoenix HTML forms&lt;/li&gt;
&lt;li&gt;LiveView forms&lt;/li&gt;
&lt;li&gt;background jobs&lt;/li&gt;
&lt;li&gt;internal application logic&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By attaching validation to the data structure instead of the request, Phoenix ensures consistent validation regardless of where data enters the system.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Phoenix does not provide a direct equivalent to Laravel FormRequests, but Ecto changesets offer a powerful and flexible alternative.&lt;/p&gt;
&lt;p&gt;Validation rules live alongside the data structure, are easily composable, and can be reused across different parts of the application. Custom rules are implemented as simple functions that transform changesets, making them easy to test and reuse.&lt;/p&gt;
&lt;p&gt;For developers moving from Laravel, the key mindset shift is moving validation from the request layer to the data layer. Once adopted, this pattern results in clean controllers, reusable validation logic, and consistent data integrity across the entire application.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/http&quot;&gt;#http&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-25T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/request-validation-in-phoenix-the-laravel-formrequest-approach</id>
    <title>🐥 Request validation in Phoenix: the Laravel FormRequest approach</title>
    <updated>2026-03-25T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/making-oban-workers-reusable-with-job-arguments"/>
    <content type="html">&lt;p&gt;When you first write an Oban worker, it&apos;s tempting to hardcode its configuration directly in the module. A worker that
fetches an RSS feed might embed the URL as a module attribute. It works fine — until you need to do the same thing for a
second feed, and suddenly you&apos;re copy-pasting a nearly identical module.&lt;/p&gt;
&lt;p&gt;There&apos;s a better way.&lt;/p&gt;
&lt;h1&gt;The Problem: One Worker, One Purpose&lt;/h1&gt;
&lt;p&gt;A typical first pass at a feed-fetching worker looks something like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Workers.FetchThinkingElixirFeedWorker&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Oban.Worker&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;queue: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:default&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;max_attempts: &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;feed_url &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;https://www.yellowduck.be/posts/feed&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;tags &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;elixir&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;phoenix&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;impl &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Oban.Worker&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Oban.Job&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;# fetch and process @feed_url, apply @tags...&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is completely fine for one feed. But the moment you want to add a second feed, you&apos;re either duplicating the module
or reaching for inheritance patterns that don&apos;t belong here.&lt;/p&gt;
&lt;h1&gt;The Fix: Pass Configuration as Job Args&lt;/h1&gt;
&lt;p&gt;Oban jobs carry an args map that gets persisted alongside the job. Instead of hardcoding configuration in the module,
move it into those args:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Workers.FetchFeedWorker&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Oban.Worker&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;queue: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:default&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;max_attempts: &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;impl &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Oban.Worker&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Oban.Job&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;args: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;feed_url&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;feed_url&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;tags&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;# fetch and process feed_url, apply tags...&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the worker is a generic mechanism. The what (which feed, which tags) is data — not code.&lt;/p&gt;
&lt;h1&gt;Using It for Scheduled Jobs&lt;/h1&gt;
&lt;p&gt;The Oban cron plugin supports passing args directly to scheduled workers, so you get the same ergonomics for recurring
jobs:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Oban.Plugins.Cron&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;crontab: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;0 * * * *&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Workers.FetchFeedWorker&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;args: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;feed_url&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;https://www.yellowduck.be/posts/feed&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&quot;tags&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;elixir&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;phoenix&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;0 * * * *&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Workers.FetchFeedWorker&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;args: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;feed_url&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;https://changelog.com/podcast/feed&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&quot;tags&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;programming&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;open-source&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two cron entries, one worker module. Adding a third feed is a config change, not a code change.&lt;/p&gt;
&lt;h1&gt;Inserting One-Off Jobs&lt;/h1&gt;
&lt;p&gt;The same pattern works for manually enqueued jobs:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;feed_url&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;https://example.com/rss&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;tags&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;news&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Workers.FetchFeedWorker&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Oban&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Why This Matters&lt;/h1&gt;
&lt;p&gt;Less code to maintain. One module handles all feeds. Bug fixes and improvements apply everywhere automatically.&lt;/p&gt;
&lt;p&gt;Clearer separation of concerns. The worker encodes how to process a feed. The job args encode which feed to process.
These are genuinely different things and should live in different places.&lt;/p&gt;
&lt;p&gt;More observable. Because args are stored in the database with each job, you can see exactly what configuration ran —
useful when debugging why a particular job behaved a certain way.&lt;/p&gt;
&lt;p&gt;Easier to extend. Want to add a max_items option? Add it to the args map and pattern match on it with a default. No new
module required.&lt;/p&gt;
&lt;h1&gt;When to Keep Workers Specific&lt;/h1&gt;
&lt;p&gt;This pattern isn&apos;t always the right call. If two &quot;similar&quot; workers actually have meaningfully different logic —
different parsing strategies, different retry behaviour, different side effects — a shared module can become a tangle of
conditionals. In that case, separate modules with a shared private helper or a behaviour is often cleaner.&lt;/p&gt;
&lt;p&gt;But when the logic is truly the same and only the inputs differ, push the inputs into args and let the worker be a
function.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-23T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/making-oban-workers-reusable-with-job-arguments</id>
    <title>🐥 Making Oban workers reusable with job arguments</title>
    <updated>2026-03-23T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/understanding-agent-genserver-task-and-ets-in-elixir"/>
    <content type="html">&lt;p&gt;When building concurrent systems in Elixir, you have several OTP abstractions available. Two of the most commonly discussed are &lt;code&gt;Agent&lt;/code&gt; and &lt;code&gt;GenServer&lt;/code&gt;, but in real systems developers also frequently use &lt;code&gt;Task&lt;/code&gt;, &lt;code&gt;ETS&lt;/code&gt;, and sometimes &lt;code&gt;GenStage&lt;/code&gt; or &lt;code&gt;Broadway&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Understanding when to use each abstraction is important for building systems that remain simple, scalable, and maintainable.&lt;/p&gt;
&lt;p&gt;This article explains the differences and ends with a common GenServer anti-pattern to avoid.&lt;/p&gt;
&lt;h1&gt;Agent: a simple state container&lt;/h1&gt;
&lt;p&gt;An &lt;code&gt;Agent&lt;/code&gt; is the simplest abstraction for managing shared state in a process.&lt;/p&gt;
&lt;p&gt;It wraps a process that holds state and provides helper functions to read or update that state.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Agent&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;start_link&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;module&quot;&gt;Agent&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;hello&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;module&quot;&gt;Agent&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stores state in a separate process&lt;/li&gt;
&lt;li&gt;Minimal API (&lt;code&gt;get&lt;/code&gt; and &lt;code&gt;update&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;No message handling&lt;/li&gt;
&lt;li&gt;No lifecycle callbacks&lt;/li&gt;
&lt;li&gt;Very small abstraction&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Typical use cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Small in-memory caches&lt;/li&gt;
&lt;li&gt;Counters&lt;/li&gt;
&lt;li&gt;Temporary shared state&lt;/li&gt;
&lt;li&gt;Test helpers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;Agent&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;start_link&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;MyCache&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;module&quot;&gt;Agent&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;MyCache&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;module&quot;&gt;Agent&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;MyCache&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An Agent is essentially &lt;strong&gt;a lightweight wrapper around a process holding state&lt;/strong&gt;.&lt;/p&gt;
&lt;h1&gt;GenServer: a full OTP server abstraction&lt;/h1&gt;
&lt;p&gt;A &lt;code&gt;GenServer&lt;/code&gt; is a behaviour for implementing long-running server processes.&lt;/p&gt;
&lt;p&gt;It provides a structured way to handle messages, maintain state, and react to events.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Counter&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;GenServer&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;start_link&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;initial&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;module&quot;&gt;GenServer&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;start_link&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constant-builtin&quot;&gt;__MODULE__&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;initial&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;constant-builtin&quot;&gt;__MODULE__&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;increment&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;module&quot;&gt;GenServer&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constant-builtin&quot;&gt;__MODULE__&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:increment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;module&quot;&gt;GenServer&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constant-builtin&quot;&gt;__MODULE__&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;handle_cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:increment&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;handle_call&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:value&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_from&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:reply&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Structured callbacks (&lt;code&gt;init&lt;/code&gt;, &lt;code&gt;handle_call&lt;/code&gt;, &lt;code&gt;handle_cast&lt;/code&gt;, &lt;code&gt;handle_info&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Supports synchronous and asynchronous communication&lt;/li&gt;
&lt;li&gt;Integrates with supervision trees&lt;/li&gt;
&lt;li&gt;Can schedule work and handle system messages&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Typical use cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stateful services&lt;/li&gt;
&lt;li&gt;Resource managers&lt;/li&gt;
&lt;li&gt;Background workers&lt;/li&gt;
&lt;li&gt;Caches with logic&lt;/li&gt;
&lt;li&gt;Rate limiters&lt;/li&gt;
&lt;li&gt;Schedulers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A GenServer is best thought of as &lt;strong&gt;a stateful actor that encapsulates behaviour&lt;/strong&gt;.&lt;/p&gt;
&lt;h1&gt;Why many developers skip Agent&lt;/h1&gt;
&lt;p&gt;Although Agents are simple, many production systems grow beyond their capabilities.&lt;/p&gt;
&lt;p&gt;Two common limitations are:&lt;/p&gt;
&lt;h3&gt;Business logic leaks outside the process&lt;/h3&gt;
&lt;p&gt;With Agents, the caller often contains the business logic:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;Agent&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;module&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With a GenServer, the process owns the behaviour:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;increment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;module&quot;&gt;GenServer&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;constant-builtin&quot;&gt;__MODULE__&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:increment&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;handle_cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:increment&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes the process behave like a &lt;strong&gt;service with a well-defined API&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Limited extensibility&lt;/h3&gt;
&lt;p&gt;Real systems often need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;periodic work&lt;/li&gt;
&lt;li&gt;cache expiration&lt;/li&gt;
&lt;li&gt;telemetry&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;batching&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are difficult to implement with Agents but natural in a GenServer.&lt;/p&gt;
&lt;p&gt;Because of this, many developers default to GenServer.&lt;/p&gt;
&lt;h1&gt;Task: concurrency for short-lived work&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;Task&lt;/code&gt; is designed for &lt;strong&gt;temporary concurrent work&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable&quot;&gt;task&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;fetch_feed&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;module&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For parallel workloads:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable&quot;&gt;urls&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;async_stream&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;function&quot;&gt;fetch_feed&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;max_concurrency: &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;to_list&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Typical use cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;parallel HTTP requests&lt;/li&gt;
&lt;li&gt;data processing&lt;/li&gt;
&lt;li&gt;CPU-bound work&lt;/li&gt;
&lt;li&gt;concurrent API calls&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tasks should be used for &lt;strong&gt;short-lived processes&lt;/strong&gt;, not long-running services.&lt;/p&gt;
&lt;h1&gt;ETS: extremely fast shared memory&lt;/h1&gt;
&lt;p&gt;ETS (Erlang Term Storage) is an in-memory storage system optimized for concurrent access.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;type&quot;&gt;:ets&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:cache&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:set&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:public&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:named_table&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;type&quot;&gt;:ets&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:cache&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:key&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;type&quot;&gt;:ets&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;lookup&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:cache&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;extremely fast&lt;/li&gt;
&lt;li&gt;concurrent reads&lt;/li&gt;
&lt;li&gt;shared memory&lt;/li&gt;
&lt;li&gt;no process bottleneck&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A common pattern is:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;GenServer&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;   ↓
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;ETS &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;table&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The GenServer manages lifecycle and policies, while ETS stores the data.&lt;/p&gt;
&lt;h1&gt;A common GenServer anti-pattern&lt;/h1&gt;
&lt;p&gt;One of the most common mistakes in Elixir systems is turning a GenServer into a &lt;strong&gt;global bottleneck&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;handle_call&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:fetch_url&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_from&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;HTTP&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:reply&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Problem:&lt;/p&gt;
&lt;p&gt;The GenServer becomes responsible for slow work such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP requests&lt;/li&gt;
&lt;li&gt;file IO&lt;/li&gt;
&lt;li&gt;database queries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because a GenServer processes &lt;strong&gt;one message at a time&lt;/strong&gt;, every request queues behind the previous one.&lt;/p&gt;
&lt;p&gt;This can severely limit concurrency.&lt;/p&gt;
&lt;h3&gt;Better approach&lt;/h3&gt;
&lt;p&gt;Use the GenServer for &lt;strong&gt;coordination&lt;/strong&gt;, not heavy work.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;handle_cast&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:fetch_url&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;module&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;fetch_and_store&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the GenServer remains responsive while tasks perform the expensive work.&lt;/p&gt;
&lt;h1&gt;Choosing the right abstraction&lt;/h1&gt;
&lt;p&gt;A simple decision guide:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Recommended abstraction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple shared state&lt;/td&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateful service or coordination&lt;/td&gt;
&lt;td&gt;GenServer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallel short-lived work&lt;/td&gt;
&lt;td&gt;Task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultra-fast shared memory&lt;/td&gt;
&lt;td&gt;ETS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming pipelines&lt;/td&gt;
&lt;td&gt;GenStage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message ingestion pipelines&lt;/td&gt;
&lt;td&gt;Broadway&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Elixir provides multiple abstractions for building concurrent systems, each designed for a specific purpose.&lt;/p&gt;
&lt;p&gt;In practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GenServer&lt;/code&gt; is the most common building block&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Task&lt;/code&gt; handles concurrent work&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ETS&lt;/code&gt; provides high-performance shared memory&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Agent&lt;/code&gt; is useful for very small state containers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Choosing the correct abstraction helps avoid bottlenecks and keeps systems simple as they grow.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/http&quot;&gt;#http&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-21T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/understanding-agent-genserver-task-and-ets-in-elixir</id>
    <title>🐥 Understanding Agent, GenServer, Task, and ETS in Elixir</title>
    <updated>2026-03-21T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/monitoring-the-progress-of-creating-an-index-in-postgresql"/>
    <content type="html">&lt;p&gt;Creating an index on a large table can take minutes or even hours. For GIN, trigram, or multi-million row tables, it can feel like nothing is happening at all.&lt;/p&gt;
&lt;p&gt;PostgreSQL provides built-in visibility into index creation progress. You don’t need external tools — just the right system view.&lt;/p&gt;
&lt;h1&gt;The &lt;code&gt;pg_stat_progress_create_index&lt;/code&gt; view&lt;/h1&gt;
&lt;p&gt;PostgreSQL exposes index build progress via:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key view is:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;pg_stat_progress_create_index&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This shows all currently running &lt;code&gt;CREATE INDEX&lt;/code&gt; operations.&lt;/p&gt;
&lt;p&gt;If nothing is building, it returns zero rows.&lt;/p&gt;
&lt;h1&gt;Example output explained&lt;/h1&gt;
&lt;p&gt;A typical query looks like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;datname&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;relid&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;regclass&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;table_name&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;index_relid&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;regclass&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;index_name&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;phase&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;lockers_total&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;lockers_done&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;blocks_total&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;blocks_done&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;tuples_total&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;tuples_done&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;pg_stat_progress_create_index&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Important columns:&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;phase&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Tells you what stage the build is in. Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;initializing&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;building index&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;waiting for writers before validation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index validation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;waiting for old snapshots&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&apos;re using &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt;, you’ll see additional validation phases.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;blocks_total&lt;/code&gt; and &lt;code&gt;blocks_done&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;These indicate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Total table blocks to scan&lt;/li&gt;
&lt;li&gt;How many have been processed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Progress percentage:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;blocks_done::&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; NULLIF&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;blocks_total&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; 0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; 100
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is usually the most reliable indicator during the scan phase.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;tuples_total&lt;/code&gt; and &lt;code&gt;tuples_done&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Shows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Estimated total rows&lt;/li&gt;
&lt;li&gt;Rows processed so far&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Useful, but block progress is often more stable.&lt;/p&gt;
&lt;h1&gt;Monitoring concurrent index builds&lt;/h1&gt;
&lt;p&gt;When using:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;INDEX&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;CONCURRENTLY&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PostgreSQL performs multiple scans and validation steps.&lt;/p&gt;
&lt;p&gt;You’ll see phases like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;building index&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;waiting for writers before validation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index validation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;waiting for old snapshots&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If it appears stuck in:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;text&quot;&gt;waiting &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;old&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; snapshots
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That usually means a long-running transaction is preventing completion.&lt;/p&gt;
&lt;p&gt;You can inspect active transactions:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;xact_start&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;pg_stat_activity&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;idle&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;xact_start&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Calculating progress percentage&lt;/h1&gt;
&lt;p&gt;A simple progress query:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;relid&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;regclass&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;table_name&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;index_relid&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;regclass&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;index_name&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;phase&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;type&quot;&gt;ROUND&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;string&quot;&gt;100.0&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;blocks_done&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;NULLIF&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;blocks_total&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number-float&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;number-float&quot;&gt;2&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;progress_percent&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;pg_stat_progress_create_index&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives you a clean percentage during the main build phase.&lt;/p&gt;
&lt;h1&gt;When progress appears frozen&lt;/h1&gt;
&lt;p&gt;Common reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Very small &lt;code&gt;maintenance_work_mem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Disk I/O saturation&lt;/li&gt;
&lt;li&gt;WAL bottlenecks&lt;/li&gt;
&lt;li&gt;Waiting on long-running transactions (concurrent builds)&lt;/li&gt;
&lt;li&gt;CPU-heavy index types (e.g. GIN with trigrams)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If blocks aren’t increasing, check:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;wait_event_type&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;wait_event&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;pg_stat_activity&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;pid&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;index_pid&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells you whether it’s waiting on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lock&lt;/li&gt;
&lt;li&gt;IO&lt;/li&gt;
&lt;li&gt;WAL&lt;/li&gt;
&lt;li&gt;Buffer pin&lt;/li&gt;
&lt;li&gt;etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Estimating total duration&lt;/h1&gt;
&lt;p&gt;A rough estimate during build:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;blocks_done&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;blocks_total&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;type&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;backend_start&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;elapsed&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;pg_stat_progress_create_index&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;pg_stat_activity&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;USING&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can extrapolate remaining time from block progress.&lt;/p&gt;
&lt;h1&gt;Version requirement&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;pg_stat_progress_create_index&lt;/code&gt; is available since:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PostgreSQL 12&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re on an older version, you won’t have native progress tracking.&lt;/p&gt;
&lt;h1&gt;Practical workflow&lt;/h1&gt;
&lt;p&gt;When building a large index in production:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;CREATE INDEX&lt;/code&gt; (or &lt;code&gt;CONCURRENTLY&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Monitor &lt;code&gt;pg_stat_progress_create_index&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Watch for blocking transactions&lt;/li&gt;
&lt;li&gt;Track I/O saturation&lt;/li&gt;
&lt;li&gt;Increase &lt;code&gt;maintenance_work_mem&lt;/code&gt; if needed&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This gives you visibility instead of guessing.&lt;/p&gt;
&lt;h1&gt;Final thoughts&lt;/h1&gt;
&lt;p&gt;Large index builds are expensive operations. Monitoring progress:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reduces uncertainty&lt;/li&gt;
&lt;li&gt;Helps detect blockers&lt;/li&gt;
&lt;li&gt;Allows time estimation&lt;/li&gt;
&lt;li&gt;Makes concurrent builds safer in production&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you regularly build large GIN or trigram indexes, having a monitoring query ready in your toolbox saves a lot of stress.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/postgresql&quot;&gt;#postgresql&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sql&quot;&gt;#sql&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-08T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/monitoring-the-progress-of-creating-an-index-in-postgresql</id>
    <title>🐥 Monitoring the progress of creating an index in PostgreSQL</title>
    <updated>2026-03-08T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/why-prefer-const-arrow-functions-over-function-declarations-in-typescript"/>
    <content type="html">&lt;p&gt;In modern TypeScript codebases, you’ll often see functions defined like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;doSomething&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;instead of:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;doSomething&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both are valid. But there are good reasons why many teams prefer the &lt;code&gt;const&lt;/code&gt; + arrow function style as a default.&lt;/p&gt;
&lt;h2&gt;Predictable execution order&lt;/h2&gt;
&lt;p&gt;Function declarations are hoisted:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;doSomething&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;// Works&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;doSomething&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can hide ordering problems and make large modules harder to reason about.&lt;/p&gt;
&lt;p&gt;Arrow functions assigned to &lt;code&gt;const&lt;/code&gt; are not callable before initialization:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;doSomething&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;// ReferenceError&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;doSomething&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes execution order explicit and avoids subtle refactoring issues.&lt;/p&gt;
&lt;h1&gt;Immutability by default&lt;/h1&gt;
&lt;p&gt;Using &lt;code&gt;const&lt;/code&gt; makes it clear the function reference should not change:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;doSomething&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Treating functions as immutable values aligns well with modern TypeScript practices and reduces accidental reassignment.&lt;/p&gt;
&lt;h1&gt;Safer &lt;code&gt;this&lt;/code&gt; behavior&lt;/h1&gt;
&lt;p&gt;Arrow functions use lexical &lt;code&gt;this&lt;/code&gt;, meaning they capture &lt;code&gt;this&lt;/code&gt; from the surrounding scope.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Example&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;42&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;function-method&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;variable-builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-builtin&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;// Works&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With a traditional function, &lt;code&gt;this&lt;/code&gt; would be undefined or unexpected unless manually bound. Arrow functions eliminate an entire class of common JavaScript bugs.&lt;/p&gt;
&lt;h1&gt;Better fit for functional patterns&lt;/h1&gt;
&lt;p&gt;Arrow functions integrate naturally with higher-order functions:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;double&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;numbers&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;double&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This style scales well in functional and compositional code.&lt;/p&gt;
&lt;h1&gt;When to use function declarations&lt;/h1&gt;
&lt;p&gt;Function declarations still make sense when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You intentionally rely on hoisting&lt;/li&gt;
&lt;li&gt;You want a clearly named recursive function&lt;/li&gt;
&lt;li&gt;You define class methods&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They’re not wrong — just more situational.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Using &lt;code&gt;const&lt;/code&gt; with arrow functions promotes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Explicit execution order&lt;/li&gt;
&lt;li&gt;Immutability&lt;/li&gt;
&lt;li&gt;Safer &lt;code&gt;this&lt;/code&gt; semantics&lt;/li&gt;
&lt;li&gt;Consistency across modern codebases&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For most TypeScript projects, it’s a sensible default. Use &lt;code&gt;function&lt;/code&gt; deliberately when its behavior is exactly what you need.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/javascript&quot;&gt;#javascript&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/typescript&quot;&gt;#typescript&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-05T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/why-prefer-const-arrow-functions-over-function-declarations-in-typescript</id>
    <title>🐥 Why prefer const arrow functions over function declarations in TypeScript?</title>
    <updated>2026-03-05T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/optimizing-nested-array-operations-in-php-from-o-3n-to-o-n"/>
    <content type="html">&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Learn how replacing Laravel&apos;s &lt;code&gt;Arr::dot()&lt;/code&gt; and &lt;code&gt;Arr::undot()&lt;/code&gt; with a recursive approach can make your nested array filtering ~3x faster while keeping the code clean and testable.&lt;/p&gt;
&lt;h1&gt;The Problem&lt;/h1&gt;
&lt;p&gt;Imagine you&apos;re building an API that returns user data with nested relationships. For privacy reasons, you need to strip out sensitive fields like password_hash from all nested objects, but you want to preserve
them at the root level for administrative views.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$userData&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;John Doe&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;$2y$10$...&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;// Keep at root&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;string&quot;&gt;&amp;#39;profile&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;string&quot;&gt;&amp;#39;bio&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;Developer&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;leaked!&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;// Remove this!&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;string&quot;&gt;&amp;#39;posts&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;title&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;My Post&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;author&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;                &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;John&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;                &lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;leaked!&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;// Remove this too!&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;            &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;The Naive Approach (Slow)&lt;/h1&gt;
&lt;p&gt;Laravel developers often reach for Arr::dot() and Arr::undot() for this kind of operation:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Illuminate&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Support&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Arr&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Illuminate&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Support&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Str&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;sanitizeData&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;array&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;// Flatten the entire array&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$dotted&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;collect&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Arr&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;dot&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// Filter out nested password_hash keys&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword-function&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;            &lt;span class=&quot;type&quot;&gt;Str&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;contains&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$key&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;.password_hash.&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;false&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;toArray&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;// Rebuild the nested structure&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Arr&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;undot&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$dotted&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why This Is Slow&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Arr::dot()&lt;/code&gt; - Flattens entire array: O(n)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filter()&lt;/code&gt; - Iterates all keys: O(n)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Arr::undot()&lt;/code&gt; - Rebuilds structure: O(n)&lt;/li&gt;
&lt;li&gt;Total: O(3n) with significant overhead&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For a typical nested structure with 1,000 elements, you&apos;re doing 3,000 operations plus the memory allocation for the flattened array.&lt;/p&gt;
&lt;h1&gt;The Optimized Approach (Fast)&lt;/h1&gt;
&lt;p&gt;Instead, use a single recursive pass:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;sanitizeData&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;array&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;removeNestedKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;isRoot&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;removeNestedKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$keyToRemove&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$isRoot&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;array&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;keyword-repeat&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$key&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// Skip the sensitive key if not at root level&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$key&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$keyToRemove&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$isRoot&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;            &lt;span class=&quot;keyword-repeat&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// Recursively process nested arrays&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;        &lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;is_array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$processed&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;removeNestedKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$keyToRemove&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;isRoot&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;            &lt;span class=&quot;comment&quot;&gt;// Only include non-empty results&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;            &lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;empty&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$processed&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;                &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$processed&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;            &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;28&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;29&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;30&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;31&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;32&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why This Is Fast&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Single pass through the data: O(n)&lt;/li&gt;
&lt;li&gt;No temporary arrays - processes in-place&lt;/li&gt;
&lt;li&gt;Lower memory usage - no flattened intermediate structure&lt;/li&gt;
&lt;li&gt;~3x faster in real-world scenarios&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Real-World Performance Test&lt;/h1&gt;
&lt;p&gt;Let&apos;s create a test with a realistic nested structure:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;test_handles_large_nested_structure_efficiently&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;// Simulate API response with 100 users, each with nested data&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$users&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;keyword-repeat&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$i&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$i&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$i&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$users&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$i&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;User &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$i&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;$2y$10$...&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;profile&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;                &lt;span class=&quot;string&quot;&gt;&amp;#39;bio&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;str_repeat&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;Lorem ipsum &amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;                &lt;span class=&quot;string&quot;&gt;&amp;#39;settings&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;                    &lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;should_be_removed&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;                    &lt;span class=&quot;string&quot;&gt;&amp;#39;notifications&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;email&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;push&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;                &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;            &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;posts&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;array_fill&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;                &lt;span class=&quot;string&quot;&gt;&amp;#39;title&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;Post title&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;                &lt;span class=&quot;string&quot;&gt;&amp;#39;author&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;                    &lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;should_be_removed&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;                    &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;Author&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;                &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;            &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;28&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;29&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$sanitized&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;sanitizeData&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;users&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$users&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;30&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;31&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;// Verify root-level password_hash is preserved&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;32&quot;&gt;    &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertArrayHasKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$sanitized&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;users&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;33&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;34&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;// Verify nested password_hash keys are removed&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;35&quot;&gt;    &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertArrayNotHasKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;36&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$sanitized&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;users&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;profile&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;settings&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;37&quot;&gt;    &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertArrayNotHasKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;38&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$sanitized&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;users&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;posts&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;author&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;39&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Complete Test Suite&lt;/h1&gt;
&lt;p&gt;Here&apos;s a comprehensive test suite covering edge cases:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;?php&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;declare&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;strict_types&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword-type&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Tests&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Services&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Services&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;DataSanitizer&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;PHPUnit&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Framework&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Attributes&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Tests&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;TestCase&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;DataSanitizerTest&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;TestCase&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;it_removes_nested_sensitive_keys&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;John&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;relations&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;                &lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;should_be_removed&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;                &lt;span class=&quot;string&quot;&gt;&amp;#39;email&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;john@example.com&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;            &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$sanitizer&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;DataSanitizer&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$sanitizer&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;sanitize&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertArrayHasKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;relations&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;28&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertArrayNotHasKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;relations&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;29&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertArrayHasKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;email&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;relations&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;30&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;31&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;32&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;33&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;it_preserves_root_level_sensitive_keys&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;34&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;35&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;36&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;keep_this&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;37&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;John&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;38&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;39&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;40&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$sanitizer&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;DataSanitizer&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;41&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$sanitizer&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;sanitize&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;42&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;43&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertArrayHasKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;44&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;keep_this&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;45&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;46&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;47&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;48&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;it_handles_deeply_nested_structures&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;49&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;50&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;51&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;level1&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;52&quot;&gt;                &lt;span class=&quot;string&quot;&gt;&amp;#39;level2&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;53&quot;&gt;                    &lt;span class=&quot;string&quot;&gt;&amp;#39;level3&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;54&quot;&gt;                        &lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;should_be_removed&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;55&quot;&gt;                        &lt;span class=&quot;string&quot;&gt;&amp;#39;safe_data&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;keep_this&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;56&quot;&gt;                    &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;57&quot;&gt;                &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;58&quot;&gt;            &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;59&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;60&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;61&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$sanitizer&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;DataSanitizer&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;62&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$sanitizer&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;sanitize&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;63&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;64&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertArrayNotHasKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;65&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;level1&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;level2&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;level3&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;66&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;keep_this&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;67&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;level1&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;level2&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;level3&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;safe_data&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;68&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;69&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;70&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;71&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;it_removes_empty_parent_keys&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;72&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;73&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;74&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;John&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;75&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;metadata&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;76&quot;&gt;                &lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;only_content&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;77&quot;&gt;            &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;78&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;79&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;80&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$sanitizer&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;DataSanitizer&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;81&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$sanitizer&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;sanitize&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;82&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;83&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// metadata should be removed entirely since it becomes empty&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;84&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertArrayNotHasKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;metadata&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;85&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertArrayHasKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;86&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;87&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;88&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;89&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;it_handles_arrays_of_objects&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;90&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;91&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;92&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;users&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;93&quot;&gt;                &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;remove&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;94&quot;&gt;                &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;remove&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;95&quot;&gt;            &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;96&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;97&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;98&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$sanitizer&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;DataSanitizer&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;99&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$sanitizer&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;sanitize&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;100&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;101&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertCount&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;users&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;102&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertArrayNotHasKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;users&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;103&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertArrayNotHasKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;users&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;104&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;105&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Generic Implementation&lt;/h1&gt;
&lt;p&gt;Here&apos;s a reusable class you can drop into any Laravel project:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;?php&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;declare&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;strict_types&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword-type&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Services&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;DataSanitizer&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;/**
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;     * Remove sensitive keys from nested data structures
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;     *
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;     * @param array&lt;string, mixed&gt; $data
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;     * @param string|array&lt;string&gt; $keysToRemove
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;     * @return array&lt;string, mixed&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;     */&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;sanitize&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;type-builtin&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;        &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$keysToRemove&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;password_hash&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$keys&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;is_array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$keysToRemove&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword-conditional-ternary&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$keysToRemove&lt;/span&gt; &lt;span class=&quot;keyword-conditional-ternary&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$keysToRemove&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;        &lt;span class=&quot;keyword-repeat&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$keys&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;removeNestedKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$key&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;isRoot&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;        &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;28&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;29&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;30&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;/**
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;31&quot;&gt;     * Recursively remove a key from nested arrays
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;32&quot;&gt;     */&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;33&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;removeNestedKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;34&quot;&gt;        &lt;span class=&quot;type-builtin&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;35&quot;&gt;        &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$keyToRemove&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;36&quot;&gt;        &lt;span class=&quot;type-builtin&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$isRoot&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;37&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;38&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;39&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;40&quot;&gt;        &lt;span class=&quot;keyword-repeat&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$data&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$key&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;41&quot;&gt;            &lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$key&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$keyToRemove&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$isRoot&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;42&quot;&gt;                &lt;span class=&quot;keyword-repeat&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;43&quot;&gt;            &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;44&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;45&quot;&gt;            &lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;is_array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;46&quot;&gt;                &lt;span class=&quot;variable&quot;&gt;$processed&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;removeNestedKey&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;47&quot;&gt;                    &lt;span class=&quot;variable&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;48&quot;&gt;                    &lt;span class=&quot;variable&quot;&gt;$keyToRemove&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;49&quot;&gt;                    &lt;span class=&quot;variable-parameter&quot;&gt;isRoot&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;false&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;50&quot;&gt;                &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;51&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;52&quot;&gt;                &lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;empty&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$processed&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;53&quot;&gt;                    &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$processed&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;54&quot;&gt;                &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;55&quot;&gt;            &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;56&quot;&gt;                &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;57&quot;&gt;            &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;58&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;59&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;60&quot;&gt;        &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;61&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;62&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Bonus: Elixir Implementation&lt;/h1&gt;
&lt;p&gt;If you&apos;re curious how this pattern translates to functional languages, here&apos;s the Elixir equivalent:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;DataSanitizer&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;comment&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;&quot;&quot;&quot;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  Remove sensitive keys from nested data structures
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &quot;&quot;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;sanitize&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key_to_remove&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;\\&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;password_hash&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;remove_nested_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key_to_remove&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;comment&quot;&gt;# Root level map - preserve the key&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;remove_nested_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key_to_remove&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;is_map&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;-&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;into: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;remove_nested_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key_to_remove&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;remove_empty_maps&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;  &lt;span class=&quot;comment&quot;&gt;# Nested map - remove the key if found&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;remove_nested_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key_to_remove&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;is_map&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;reject&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_v&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;k&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key_to_remove&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;remove_nested_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key_to_remove&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;into&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;remove_empty_maps&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;  &lt;span class=&quot;comment&quot;&gt;# List - recursively process each element&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;remove_nested_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key_to_remove&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_is_root&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;is_list&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;28&quot;&gt;    &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;remove_nested_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;key_to_remove&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;29&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;30&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;31&quot;&gt;  &lt;span class=&quot;comment&quot;&gt;# Primitive value - pass through&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;32&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;remove_nested_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_key_to_remove&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_is_root&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;33&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;34&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;remove_empty_maps&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;is_map&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;35&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;map&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;36&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;reject&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;_k&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;is_map&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;map_size&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;37&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;into&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;38&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;39&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Key Takeaways&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;Think about complexity - Just because Laravel provides a helper doesn&apos;t mean it&apos;s the most efficient for your use case&lt;/li&gt;
&lt;li&gt;Measure real-world performance - Test with realistic data sizes&lt;/li&gt;
&lt;li&gt;Recursive solutions are often more efficient than flatten-filter-rebuild patterns&lt;/li&gt;
&lt;li&gt;Write comprehensive tests - Edge cases matter, especially with nested data&lt;/li&gt;
&lt;li&gt;Make it reusable - Generic implementations pay dividends across projects&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;When to Use Each Approach&lt;/h1&gt;
&lt;p&gt;Use &lt;code&gt;Arr::dot()&lt;/code&gt; / &lt;code&gt;Arr::undot()&lt;/code&gt; when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Working with small datasets (&lt; 100 elements)&lt;/li&gt;
&lt;li&gt;Code clarity is more important than performance&lt;/li&gt;
&lt;li&gt;One-off operations in migrations or seeders&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use recursive approach when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Processing large nested structures&lt;/li&gt;
&lt;li&gt;Operation runs frequently (API responses, event processing)&lt;/li&gt;
&lt;li&gt;Performance matters (real-time systems, high-traffic APIs)&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-03-01T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/optimizing-nested-array-operations-in-php-from-o-3n-to-o-n</id>
    <title>🐥 Optimizing nested array operations in PHP: from O(3n) to O(n)</title>
    <updated>2026-03-01T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/cumulative-monthly-growth-queries-in-mysql-postgresql-and-phoenix-ecto"/>
    <content type="html">&lt;p&gt;When reporting growth over time, grouping by month alone is usually not enough. For charts and dashboards you typically want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Missing months to appear explicitly with a count of &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;A cumulative total to show growth over time&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Below are idiomatic solutions for &lt;strong&gt;MySQL&lt;/strong&gt;, &lt;strong&gt;PostgreSQL&lt;/strong&gt;, and &lt;strong&gt;Phoenix Ecto&lt;/strong&gt;.&lt;/p&gt;
&lt;h1&gt;MySQL 8+&lt;/h1&gt;
&lt;p&gt;MySQL does not have a built-in series generator, so we use a recursive CTE.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;WITH&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;RECURSIVE&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;months&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;DATE_FORMAT&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;MIN&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;created_at&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;%Y-%m-01&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;month&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;your_table&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;keyword-operator&quot;&gt;UNION&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;ALL&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;DATE_ADD&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;INTERVAL&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; 1 &lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;MONTH&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;months&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;month&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;DATE_FORMAT&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;CURDATE&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;%Y-%m-01&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;type&quot;&gt;monthly_counts&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;type&quot;&gt;DATE_FORMAT&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;created_at&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;%Y-%m-01&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;type&quot;&gt;COUNT&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;monthly_count&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;your_table&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;month&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;  &lt;span class=&quot;type&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;  &lt;span class=&quot;type&quot;&gt;COALESCE&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;mc&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;monthly_count&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number-float&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;monthly_count&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;  &lt;span class=&quot;type&quot;&gt;SUM&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;COALESCE&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;mc&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;monthly_count&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number-float&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;OVER&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;cumulative_count&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;months&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;m&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;LEFT&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;monthly_counts&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;mc&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;USING&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Requires MySQL 8.0+&lt;/li&gt;
&lt;li&gt;Recursive CTE generates missing months&lt;/li&gt;
&lt;li&gt;Window function calculates the running total&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;PostgreSQL&lt;/h1&gt;
&lt;p&gt;PostgreSQL provides &lt;code&gt;generate_series&lt;/code&gt;, which makes this much simpler.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;WITH&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;months&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;generate_series&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;type&quot;&gt;date_trunc&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;month&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;MIN&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;created_at&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;type&quot;&gt;date_trunc&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;month&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;CURRENT_DATE&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;type-builtin&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; &amp;#39;1 month&amp;#39;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;month&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;your_table&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;type&quot;&gt;monthly_counts&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;type&quot;&gt;date_trunc&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;month&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;created_at&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;type&quot;&gt;COUNT&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;monthly_count&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;your_table&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;month&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;  &lt;span class=&quot;type&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;  &lt;span class=&quot;type&quot;&gt;COALESCE&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;mc&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;monthly_count&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number-float&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;monthly_count&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;  &lt;span class=&quot;type&quot;&gt;SUM&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;COALESCE&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;mc&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;monthly_count&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number-float&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;OVER&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;cumulative_count&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;months&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;m&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;LEFT&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;monthly_counts&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;mc&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;USING&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;generate_series&lt;/code&gt; replaces the recursive CTE&lt;/li&gt;
&lt;li&gt;&lt;code&gt;date_trunc(&apos;month&apos;, ...)&lt;/code&gt; is the idiomatic grouping method&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Phoenix Ecto (PostgreSQL)&lt;/h1&gt;
&lt;p&gt;Ecto does not have a native abstraction for time series generation, but PostgreSQL’s strengths can still be used via &lt;code&gt;fragment/1&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Example schema&lt;/h2&gt;
&lt;p&gt;Assume a schema with a &lt;code&gt;created_at&lt;/code&gt; timestamp:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;schema&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;items&quot;&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;timestamps&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Monthly counts with missing months and cumulative total&lt;/h2&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable&quot;&gt;query&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;m&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;fragment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&quot;&quot;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    SELECT generate_series(
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;      date_trunc(&amp;#39;month&amp;#39;, (SELECT MIN(created_at) FROM items)),
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;      date_trunc(&amp;#39;month&amp;#39;, CURRENT_DATE),
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      interval &amp;#39;1 month&amp;#39;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    )::date AS month
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;left_join: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;fragment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&quot;&quot;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    SELECT
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;      date_trunc(&amp;#39;month&amp;#39;, created_at)::date AS month,
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;      COUNT(*) AS monthly_count
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    FROM items
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;    GROUP BY 1
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;  &quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;on: &lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;fragment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;? = ?&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;select: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;month: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;monthly_count: &lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;coalesce&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;monthly_count&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;cumulative_count:
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;&lt;/span&gt;      &lt;span class=&quot;function-call&quot;&gt;fragment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;        &lt;span class=&quot;string&quot;&gt;&quot;SUM(COALESCE(?, 0)) OVER (ORDER BY ?)&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;monthly_count&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;month&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;order_by: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;month&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Notes for Ecto&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Uses PostgreSQL-specific SQL via &lt;code&gt;fragment/1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Keeps the time series generation in the database&lt;/li&gt;
&lt;li&gt;Returns a clean structure ready for charts or LiveView updates&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Summary&lt;/h1&gt;
&lt;p&gt;Across MySQL, PostgreSQL, and Phoenix Ecto, the core idea is the same:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generate a complete month series&lt;/li&gt;
&lt;li&gt;Aggregate real data per month&lt;/li&gt;
&lt;li&gt;Left join and fill missing values with zero&lt;/li&gt;
&lt;li&gt;Use a window function for cumulative growth&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once you have this pattern in place, producing accurate growth charts becomes straightforward and reliable.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/postgresql&quot;&gt;#postgresql&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/mysql&quot;&gt;#mysql&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sql&quot;&gt;#sql&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-02-22T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/cumulative-monthly-growth-queries-in-mysql-postgresql-and-phoenix-ecto</id>
    <title>🐥 Cumulative monthly growth queries in MySQL, PostgreSQL, and Phoenix Ecto</title>
    <updated>2026-02-22T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/working-with-date-ranges-in-elixir-a-practical-guide"/>
    <content type="html">&lt;p&gt;When building applications, you often need to iterate over a range of dates—whether it&apos;s for generating reports, scheduling tasks, or processing time-series data. If you&apos;re coming from languages like PHP or Ruby, you might reach for external libraries. In Elixir, the solution is elegantly built into the standard library.&lt;/p&gt;
&lt;h1&gt;The basics: &lt;code&gt;Date.range/2&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;Elixir provides &lt;code&gt;Date.range/2&lt;/code&gt; out of the box, which creates an enumerable range of dates. Here&apos;s the simplest example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;2026-01-26&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;2026-02-08&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;variable&quot;&gt;dates&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;to_list&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# =&gt; [~D[2026-01-26], ~D[2026-01-27], ..., ~D[2026-02-08]]&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;~D&lt;/code&gt; sigil creates a &lt;code&gt;Date&lt;/code&gt; struct at compile time, making it both performant and type-safe.&lt;/p&gt;
&lt;h1&gt;Iterating over dates&lt;/h1&gt;
&lt;p&gt;Since &lt;code&gt;Date.range/2&lt;/code&gt; returns an enumerable, you can use all your favourite &lt;code&gt;Enum&lt;/code&gt; functions:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;each&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;puts&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;Processing &lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;#&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;comment&quot;&gt;# Your business logic here&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This lazy evaluation means you&apos;re not creating a massive list in memory—Elixir generates each date as needed.&lt;/p&gt;
&lt;h1&gt;Practical use case: Phoenix mix task&lt;/h1&gt;
&lt;p&gt;Here&apos;s how you might use this in a real Phoenix application, perhaps for a scheduled job that needs to backfill data:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Mix.Tasks.BackfillReports&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Mix.Task&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;comment-documentation&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;comment-documentation&quot;&gt;shortdoc&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Backfill reports for a date range&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;_args&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;module&quot;&gt;Mix.Task&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;app.start&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;2026-01-01&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;2026-01-31&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;each&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;      &lt;span class=&quot;module&quot;&gt;MyApp.Reports&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;generate_daily_report&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;      &lt;span class=&quot;module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;puts&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;✓ Generated report for &lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;#&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Advanced patterns&lt;/h1&gt;
&lt;h2&gt;Stepping through dates&lt;/h2&gt;
&lt;p&gt;Need every other day? Or every week? Just add a step parameter:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# Every 7 days (weekly)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;to_list&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# Backwards iteration&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;to_list&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Filtering weekdays&lt;/h2&gt;
&lt;p&gt;Want to process only business days? Chain with Enum.filter/2:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;day_of_week&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;5&lt;/span&gt;  &lt;span class=&quot;comment&quot;&gt;# Monday = 1, Sunday = 7&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;each&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;function&quot;&gt;process_business_day&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Parsing from user input&lt;/h2&gt;
&lt;p&gt;When working with dynamic dates from APIs or user input:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;-&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;from_iso8601&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;2026-01-26&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;-&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;from_iso8601&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;2026-02-08&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;to_list&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Invalid date format&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Performance considerations&lt;/h1&gt;
&lt;p&gt;One of the beautiful things about &lt;code&gt;Date.range/2&lt;/code&gt; is its lazy evaluation. When you write:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;2020-01-01&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;2026-12-31&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;take&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Elixir only generates the first 10 dates, not all ~2500 dates in the range. This makes it memory-efficient even for large ranges.&lt;/p&gt;
&lt;h1&gt;Comparison with Other Languages&lt;/h1&gt;
&lt;p&gt;Coming from PHP/Laravel? This is your &lt;code&gt;CarbonPeriod&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$period&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;CarbonPeriod&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$start&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This translates to Elixir as:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable&quot;&gt;period&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From Ruby/Rails? This replaces manual iteration:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-ruby&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Elixir, this is essentially the same:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;end_date&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;each&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Elixir&apos;s standard library provides powerful, memory-efficient date range functionality without needing external dependencies. The combination of &lt;code&gt;Date.range/2&lt;/code&gt; and the &lt;code&gt;Enum&lt;/code&gt; module gives you all the tools you need for date manipulation in a functional, composable way.&lt;/p&gt;
&lt;p&gt;Next time you need to iterate over dates in your Phoenix app, remember: it&apos;s just &lt;code&gt;Date.range/2&lt;/code&gt; away.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-02-15T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/working-with-date-ranges-in-elixir-a-practical-guide</id>
    <title>🐥 Working with date ranges in Elixir: a practical guide</title>
    <updated>2026-02-15T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/checking-whether-an-ip-address-is-internal"/>
    <content type="html">&lt;p&gt;When building SaaS applications, it is common to treat internal (private or reserved) IP addresses differently from public ones. Typical examples are rate limiting, audit logging, or skipping geo-IP lookups for localhost traffic.&lt;/p&gt;
&lt;p&gt;This post shows how to check whether an IP address is internal, starting from an Elixir example and then translating the same idea to PHP.&lt;/p&gt;
&lt;h1&gt;The Elixir way&lt;/h1&gt;
&lt;p&gt;In Elixir, the standard library provides &lt;code&gt;:inet.parse_address/1&lt;/code&gt; to validate IP addresses. From there, you can pattern match on the octets to exclude private and reserved ranges.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;IPUtils&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;public_ipv4?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;ip&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;is_binary&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;ip&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;:inet&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;parse_address&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;to_charlist&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;ip&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_c&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_d&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;        &lt;span class=&quot;keyword-operator&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;private_or_reserved?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;boolean&quot;&gt;false&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;private_or_reserved?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;private_or_reserved?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;127&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;private_or_reserved?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;192&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;168&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;private_or_reserved?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;172&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;16&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;=&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;31&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;private_or_reserved?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;false&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;&lt;span class=&quot;module&quot;&gt;IPUtils&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;public_ipv4?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;127.0.0.1&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;# false&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;&lt;span class=&quot;module&quot;&gt;IPUtils&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;public_ipv4?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;8.8.8.8&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;   &lt;span class=&quot;comment&quot;&gt;# true&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pattern matching keeps the intent clear and makes it easy to extend this logic later if you want to support IPv6 or additional ranges.&lt;/p&gt;
&lt;h1&gt;The PHP approach&lt;/h1&gt;
&lt;p&gt;PHP ships with a very convenient helper: &lt;code&gt;filter_var&lt;/code&gt;. Combined with the right flags, it allows you to validate &lt;em&gt;only&lt;/em&gt; public IPv4 addresses.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$user_ip&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;127.0.0.1&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$is_public&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;filter_var&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$user_ip&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;FILTER_VALIDATE_IP&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; FILTER_FLAG_IPV4 &lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;FILTER_FLAG_NO_PRIV_RANGE&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;FILTER_FLAG_NO_RES_RANGE&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$is_public&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;// Internal, private, or reserved IP&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;// Public IPv4 address&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What this does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FILTER_VALIDATE_IP&lt;/code&gt; checks that the value is a valid IP address.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FILTER_FLAG_IPV4&lt;/code&gt; restricts the check to IPv4.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FILTER_FLAG_NO_PRIV_RANGE&lt;/code&gt; excludes private ranges such as &lt;code&gt;10.0.0.0/8&lt;/code&gt; and &lt;code&gt;192.168.0.0/16&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FILTER_FLAG_NO_RES_RANGE&lt;/code&gt; excludes reserved ranges like &lt;code&gt;127.0.0.0/8&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the function returns &lt;code&gt;false&lt;/code&gt;, the IP is either invalid or internal/reserved.&lt;/p&gt;
&lt;h1&gt;Closing thoughts&lt;/h1&gt;
&lt;p&gt;PHP’s &lt;code&gt;filter_var&lt;/code&gt; is hard to beat for conciseness, but the same idea translates cleanly to other ecosystems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Validate the IP address first.&lt;/li&gt;
&lt;li&gt;Explicitly exclude private and reserved ranges.&lt;/li&gt;
&lt;li&gt;Treat everything else as public.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keeping this logic centralized (for example in a small utility module) helps ensure consistent behavior across your application, regardless of the language you are using.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/logging&quot;&gt;#logging&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-02-08T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/checking-whether-an-ip-address-is-internal</id>
    <title>🐥 Checking whether an IP address is internal</title>
    <updated>2026-02-08T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/using-pdftoppm-from-elixir-to-convert-pdf-files-to-images"/>
    <content type="html">&lt;p&gt;When you need to convert PDF files to images on a Linux server, &lt;code&gt;pdftoppm&lt;/code&gt; (from the Poppler utilities) is a fast and reliable tool. In this post, we’ll look at how to invoke &lt;code&gt;pdftoppm&lt;/code&gt; from Elixir and how to run multiple conversions in parallel to improve throughput.&lt;/p&gt;
&lt;h1&gt;Installing pdftoppm&lt;/h1&gt;
&lt;p&gt;On most Linux distributions, &lt;code&gt;pdftoppm&lt;/code&gt; is part of the &lt;code&gt;poppler-utils&lt;/code&gt; package, on macOS, it&apos;s simply &lt;code&gt;poppler&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# Debian / Ubuntu&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;poppler-utils&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# Alpine&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;apk&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;poppler-utils&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# macOS&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;poppler&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can verify the installation with:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;pdftoppm&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-h&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Basic pdftoppm usage&lt;/h1&gt;
&lt;p&gt;To convert a PDF to JPEG images at 150 DPI:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;pdftoppm&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-jpeg&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;150&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;input.pdf&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;output/page&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This produces files like:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;output/page-1.jpg&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;output/page-2.jpg&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each page becomes a separate image.&lt;/p&gt;
&lt;h1&gt;Writing image data to stdout with pdftoppm&lt;/h1&gt;
&lt;p&gt;In some setups it is useful to avoid temporary files and let &lt;code&gt;pdftoppm&lt;/code&gt; write the rendered image directly to &lt;code&gt;stdout&lt;/code&gt;. From Elixir, you can then capture that output and persist it yourself. This post shows how to do this cleanly, while keeping &lt;code&gt;stdout&lt;/code&gt; and &lt;code&gt;stderr&lt;/code&gt; separated so errors are easy to handle.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pdftoppm&lt;/code&gt; writes images to files by default, but if you don&apos;t pass the &lt;code&gt;PPM-file-prefix&lt;/code&gt; it will write the image data to &lt;code&gt;stdout&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To render a single page as JPEG to &lt;code&gt;stdout&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;pdftoppm&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-jpeg&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;150&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-jpegopt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;quality=85&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-aa&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-aaVector&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;input.pdf&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On success:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stdout&lt;/code&gt; contains the binary JPEG data&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stderr&lt;/code&gt; is empty&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On failure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stdout&lt;/code&gt; is empty&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stderr&lt;/code&gt; contains the error message&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes it a good fit for piping and programmatic use.&lt;/p&gt;
&lt;h1&gt;Why &lt;code&gt;System.cmd/3&lt;/code&gt; is not enough&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;System.cmd/3&lt;/code&gt; can redirect &lt;code&gt;stderr&lt;/code&gt; to &lt;code&gt;stdout&lt;/code&gt;, but it cannot capture them separately. Since we explicitly want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;image data from &lt;code&gt;stdout&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;error messages from &lt;code&gt;stderr&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;we need to use a &lt;code&gt;Port&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Converting a single page from Elixir&lt;/h1&gt;
&lt;p&gt;The function below renders a single page to JPEG, saves the image to disk, and returns structured errors when something goes wrong.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;PdfToImage&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;convert_page&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pdf_path&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;output_file&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;opts&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;\\&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;dpi&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Keyword&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:dpi&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;150&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;args&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;pdftoppm&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;-jpeg&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;-jpegopt&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;quality=85&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;-aa&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;yes&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;-aaVector&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;yes&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;-r&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dpi&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;-f&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;-l&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;      &lt;span class=&quot;variable&quot;&gt;pdf_path&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;port&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;      &lt;span class=&quot;module&quot;&gt;Port&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:spawn_executable&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;find_executable&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;pdftoppm&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:binary&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:exit_status&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;args: &lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;tl&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;collect_output&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;output_file&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;collect_output&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;output_file&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;stdout&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;stderr&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;receive&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;28&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;29&quot;&gt;        &lt;span class=&quot;function-call&quot;&gt;collect_output&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;output_file&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;stdout&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;stderr&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;30&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;31&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:exit_status&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;32&quot;&gt;        &lt;span class=&quot;module&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;write!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;output_file&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;stdout&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;33&quot;&gt;        &lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;34&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;35&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:exit_status&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;36&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;stderr&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;37&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;after&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;38&quot;&gt;      &lt;span class=&quot;number&quot;&gt;30_000&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;39&quot;&gt;        &lt;span class=&quot;module&quot;&gt;Port&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;40&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:timeout&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;41&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;42&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;43&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Usage:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;module&quot;&gt;PdfToImage&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;convert_page&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;string&quot;&gt;&quot;input.pdf&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;string&quot;&gt;&quot;output/page-1.jpg&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;dpi: &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;200&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Parallelizing page conversion&lt;/h1&gt;
&lt;p&gt;Because each page conversion is independent, this approach works well with &lt;code&gt;Task.async_stream/3&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable&quot;&gt;pages&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;10&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;module&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;async_stream&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;pages&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;page&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;module&quot;&gt;PdfToImage&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;convert_page&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;input.pdf&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;variable&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;output/page-&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;#&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;.jpg&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;max_concurrency: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;schedulers_online&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;timeout: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:infinity&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;&lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;to_list&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each task spawns its own &lt;code&gt;pdftoppm&lt;/code&gt; process, captures binary image data from &lt;code&gt;stdout&lt;/code&gt;, and only writes a file once rendering succeeds.&lt;/p&gt;
&lt;h1&gt;Error handling characteristics&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;On success, only &lt;code&gt;stdout&lt;/code&gt; is used and written to disk&lt;/li&gt;
&lt;li&gt;On failure, no file is created&lt;/li&gt;
&lt;li&gt;The returned error contains the full &lt;code&gt;stderr&lt;/code&gt; output from &lt;code&gt;pdftoppm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;This makes it suitable for background jobs and structured logging&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;By letting &lt;code&gt;pdftoppm&lt;/code&gt; write image data to &lt;code&gt;stdout&lt;/code&gt; and capturing it via a &lt;code&gt;Port&lt;/code&gt;, you gain full control over I/O, error handling, and parallel execution. This avoids temporary files, keeps failure cases clean, and integrates well with Elixir’s concurrency primitives.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/pdf&quot;&gt;#pdf&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/linux&quot;&gt;#linux&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-02-01T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/using-pdftoppm-from-elixir-to-convert-pdf-files-to-images</id>
    <title>🐥 Using pdftoppm from Elixir to convert PDF files to images</title>
    <updated>2026-02-01T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/extracting-unique-ids-from-json-arrays-in-mysql"/>
    <content type="html">&lt;p&gt;When working with MySQL, it’s common to store arrays of IDs as JSON inside a column. For example, you might have a table that caches data and keeps a list of related IDs in a JSON column. Over time, you might want to extract a unique list of all these IDs across the table.&lt;/p&gt;
&lt;p&gt;Consider a generic table like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;`example_cache`&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;`id`&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;BIGINT&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;UNSIGNED&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;AUTO_INCREMENT&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;`entity_id`&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;BIGINT&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;UNSIGNED&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;`cache_key`&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;VARCHAR&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;191&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;`related_ids`&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;JSON&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;`created_at`&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;TIMESTAMP&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;`updated_at`&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;TIMESTAMP&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;KEY&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;`id`&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;keyword-modifier&quot;&gt;UNIQUE&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; `example_cache_entity_id_cache_key_unique` &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;`entity_id`&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; `cache_key`&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; `example_cache_entity_id_index` &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;`entity_id`&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;ENGINE&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;InnoDB &lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;DEFAULT&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; CHARSET&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;utf8mb4&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, the &lt;code&gt;related_ids&lt;/code&gt; column contains arrays of IDs:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-json&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;101&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;102&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;103&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;104&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;105&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In MySQL 8.0+, the cleanest way to get a unique list of IDs is to use &lt;code&gt;JSON_TABLE&lt;/code&gt; with &lt;code&gt;DISTINCT&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;DISTINCT&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;jt&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;related_id&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;example_cache&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;ec&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;JSON_TABLE&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;type&quot;&gt;ec&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;related_ids&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;string&quot;&gt;&amp;#39;$[*]&amp;#39;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;COLUMNS&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;related_id&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;BIGINT&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;PATH &amp;#39;$&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; jt
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;ec&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;related_ids&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;How it works&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;JSON_TABLE(... &apos;$[*]&apos;)&lt;/code&gt; converts each JSON array into individual rows. Each element becomes a row in the virtual table &lt;code&gt;jt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DISTINCT&lt;/code&gt; ensures duplicate IDs across multiple rows are removed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WHERE ec.related_ids IS NOT NULL&lt;/code&gt; filters out rows without any IDs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want unique IDs for a specific entity:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;DISTINCT&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;jt&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;related_id&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;example_cache&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;ec&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;JSON_TABLE&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;type&quot;&gt;ec&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;related_ids&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;string&quot;&gt;&amp;#39;$[*]&amp;#39;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;COLUMNS&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;related_id&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;BIGINT&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;PATH &amp;#39;$&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; jt
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;ec&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;entity_id&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;42&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword-operator&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;ec&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;related_ids&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also aggregate the results back into a single JSON array:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;JSON_ARRAYAGG&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;DISTINCT&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;jt&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;related_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;related_ids&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;example_cache&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;ec&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;JOIN&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;JSON_TABLE&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;type&quot;&gt;ec&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;related_ids&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;string&quot;&gt;&amp;#39;$[*]&amp;#39;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;COLUMNS&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;related_id&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;BIGINT&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;PATH &amp;#39;$&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;AS&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; jt
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;ec&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;related_ids&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Considerations&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This approach requires &lt;strong&gt;MySQL 8.0.4+&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If your &lt;code&gt;related_ids&lt;/code&gt; arrays grow large, consider normalizing them into a separate join table. Querying and indexing will be more efficient than querying JSON.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JSON_TABLE&lt;/code&gt; provides a clean way to explode arrays without relying on string manipulation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This method is a practical and idiomatic way to extract and deduplicate JSON array elements in MySQL.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/mysql&quot;&gt;#mysql&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sql&quot;&gt;#sql&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-01-20T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/extracting-unique-ids-from-json-arrays-in-mysql</id>
    <title>🐥 Extracting unique IDs from JSON arrays in MySQL</title>
    <updated>2026-01-20T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/creating-relative-urls-from-absolute-urls-in-elixir"/>
    <content type="html">&lt;p&gt;When working with redirects, internal links, or LiveView navigation, you often want to turn an absolute URL into a relative one. Given a URL like:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;https://example.com/some/path?page=2#section
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the goal is to end up with:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;/some/path?page=2#section
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;The non-idiomatic approach&lt;/h1&gt;
&lt;p&gt;A common first attempt is to manually concatenate &lt;code&gt;path&lt;/code&gt;, &lt;code&gt;query&lt;/code&gt;, and &lt;code&gt;fragment&lt;/code&gt; after parsing the URL. While this works, it pushes URL semantics into string logic and is easy to get wrong.&lt;/p&gt;
&lt;p&gt;Elixir’s standard library gives us a cleaner option.&lt;/p&gt;
&lt;h1&gt;The idiomatic solution using &lt;code&gt;URI&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;The &lt;code&gt;URI&lt;/code&gt; module is designed so that parsing and serialization are inverse operations. The trick is to parse the URL, drop the parts you don’t need, and convert it back to a string.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;https://example.com/some/path?page=2#section&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;relative&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;URI&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;take&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:path&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:query&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:fragment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;struct&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;URI&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;URI&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This produces:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;string&quot;&gt;&quot;/some/path?page=2#section&quot;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Why this approach works well&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;No manual handling of &lt;code&gt;?&lt;/code&gt; or &lt;code&gt;#&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Correct behavior when &lt;code&gt;query&lt;/code&gt; or &lt;code&gt;fragment&lt;/code&gt; is missing&lt;/li&gt;
&lt;li&gt;Clear intent: keep only the parts relevant for a relative URL&lt;/li&gt;
&lt;li&gt;Uses only Elixir’s standard library&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;A small helper function&lt;/h1&gt;
&lt;p&gt;If you need this in more than one place, wrapping it in a helper keeps your codebase tidy:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;relative_url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;URI&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;take&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:path&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:query&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:fragment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;struct&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;URI&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;URI&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern keeps URL handling declarative and avoids brittle string manipulation, which is exactly what the &lt;code&gt;URI&lt;/code&gt; module is there for.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-01-15T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/creating-relative-urls-from-absolute-urls-in-elixir</id>
    <title>🐥 Creating relative URLs from absolute URLs in Elixir</title>
    <updated>2026-01-15T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/using-tailwind-css-group-hover-to-style-child-elements"/>
    <content type="html">&lt;p&gt;Tailwind CSS provides the &lt;code&gt;group&lt;/code&gt; and &lt;code&gt;group-hover&lt;/code&gt; utilities to conditionally apply styles to child elements based on the state of a parent. This is especially useful when you want to change the appearance of nested elements when a container is hovered, focused, or active.&lt;/p&gt;
&lt;p&gt;In plain CSS, styling a child element based on the hover state of its parent typically requires a selector like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-css&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;card&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;hover&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;card-icon&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; red&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With utility-first CSS, you want to avoid custom selectors and keep everything inline in your markup.&lt;/p&gt;
&lt;h1&gt;The &lt;code&gt;group&lt;/code&gt; utility&lt;/h1&gt;
&lt;p&gt;Tailwind solves this with the &lt;code&gt;group&lt;/code&gt; class. You mark the parent element as a group, and then reference that state from any descendant using &lt;code&gt;group-hover:*&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-html&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;group p-4 border rounded&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;span&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;text-gray-400 group-hover:text-red-500&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    Hover me
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;span&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the parent &lt;code&gt;div&lt;/code&gt; is hovered, the &lt;code&gt;span&lt;/code&gt; receives the &lt;code&gt;text-red-500&lt;/code&gt; class.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;group&lt;/code&gt; class itself does not add any styles. It only acts as a named hook that allows child elements to react to the parent’s state. Tailwind expands &lt;code&gt;group-hover:text-red-500&lt;/code&gt; into a CSS selector that targets the child when the parent is hovered.&lt;/p&gt;
&lt;p&gt;This keeps your behavior explicit in the markup without writing custom CSS.&lt;/p&gt;
&lt;h1&gt;Beyond hover&lt;/h1&gt;
&lt;p&gt;The same pattern works for other state variants:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-html&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;group focus-within:border-blue-500&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;border p-2&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;/&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;text-sm text-gray-400 group-focus-within:text-blue-500&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    Input is focused
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Commonly used variants include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;group-hover&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;group-focus&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;group-focus-within&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;group-active&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Named groups&lt;/h1&gt;
&lt;p&gt;When you have nested groups, you can give them explicit names to avoid conflicts:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-html&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;group/card&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;h3&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;group-hover/card:underline&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    Card title
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;h3&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes it clear which parent controls which child styles.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Tailwind’s &lt;code&gt;group&lt;/code&gt; utilities let child elements respond to parent state changes without custom CSS. By marking a parent as a group and using &lt;code&gt;group-hover&lt;/code&gt; (or related variants) on descendants, you can build rich interactive components while staying within Tailwind’s utility-first approach.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/css&quot;&gt;#css&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/html&quot;&gt;#html&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-01-08T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/using-tailwind-css-group-hover-to-style-child-elements</id>
    <title>🐥 Using Tailwind CSS group hover to style child elements</title>
    <updated>2026-01-08T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/sorting-case-insensitive-in-sqlite-with-collate-nocase"/>
    <content type="html">&lt;p&gt;SQLite sorts text using a collation. By default this is case sensitive, which often leads to surprising orderings when data contains mixed casing. SQLite provides a built-in solution for simple use cases: &lt;code&gt;COLLATE NOCASE&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Basic usage&lt;/h1&gt;
&lt;p&gt;You can apply &lt;code&gt;COLLATE NOCASE&lt;/code&gt; directly in an &lt;code&gt;ORDER BY&lt;/code&gt; clause:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;name&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;users&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; COLLATE NOCASE&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This sorts &lt;code&gt;Alice&lt;/code&gt;, &lt;code&gt;bob&lt;/code&gt;, and &lt;code&gt;charlie&lt;/code&gt; as you would typically expect, regardless of their original casing.&lt;/p&gt;
&lt;p&gt;You can also use it in comparisons:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;users&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;pieter&amp;#39;&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;COLLATE&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; NOCASE&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Defining it at column level&lt;/h1&gt;
&lt;p&gt;If a column is almost always queried case-insensitively, you can define the collation in the schema:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;INTEGER&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; COLLATE NOCASE
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All comparisons and sorting on &lt;code&gt;name&lt;/code&gt; will now default to case-insensitive behavior.&lt;/p&gt;
&lt;h1&gt;Indexes and performance&lt;/h1&gt;
&lt;p&gt;Indexes are collation-aware in SQLite. If you sort case-insensitively, make sure your index uses the same collation:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;INDEX&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; users_name_nocase_idx
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;&lt;span class=&quot;keyword-operator&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;name &lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;COLLATE&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; NOCASE&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without this, SQLite may ignore the index for &lt;code&gt;ORDER BY name COLLATE NOCASE&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Limitations to be aware of&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;COLLATE NOCASE&lt;/code&gt; is ASCII-only. It handles &lt;code&gt;A–Z&lt;/code&gt; and &lt;code&gt;a–z&lt;/code&gt;, but does not perform full Unicode case folding. Characters like &lt;code&gt;é&lt;/code&gt; or &lt;code&gt;ß&lt;/code&gt; are not handled correctly.&lt;/p&gt;
&lt;p&gt;If you need proper internationalized sorting, you have a few options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Normalize and store a secondary, lowercased column&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;ORDER BY LOWER(name)&lt;/code&gt; (simple but index-unfriendly)&lt;/li&gt;
&lt;li&gt;Compile SQLite with the ICU extension and use ICU collations&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;When to use it&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;COLLATE NOCASE&lt;/code&gt; is a pragmatic choice when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your data is primarily ASCII&lt;/li&gt;
&lt;li&gt;You want predictable, simple case-insensitive sorting&lt;/li&gt;
&lt;li&gt;You care about index-backed performance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For many applications, it is the right balance between correctness and simplicity.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sql&quot;&gt;#sql&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sqlite&quot;&gt;#sqlite&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-01-06T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/sorting-case-insensitive-in-sqlite-with-collate-nocase</id>
    <title>🐥 Sorting case insensitive in SQLite with COLLATE NOCASE</title>
    <updated>2026-01-06T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/be-aware-of-1password-breaking-syntax-highlighting"/>
    <content type="html">&lt;p&gt;You might have noticed that the syntax highlighting of the code snippets on this site don&apos;t always work correctly.&lt;/p&gt;
&lt;p&gt;After spending a couple of hours figuring out what happened, it turns out that the culprit is 1Password&apos;s browser extension.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hey everyone! I want to thank everyone who called our attention to this and explain what happened and what we’re doing about it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt; Prism.js is a syntax-highlighting library we use for our Labs Snippets feature. While optimizing our build to reduce bundle size, we unintentionally bundled Prism.js into the extension in a way that caused it to run on pages where it shouldn’t, which interfered with code formatting on certain sites. We apologize for the inconvenience this caused.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What we’re doing about it:&lt;/strong&gt;  We’ve completed the fix and submitted it to the Chrome Web Store, along with Firefox, Edge, and our other supported extension storefronts. Rollout timing depends on each store’s review process, but we expect it to land over the next few days.&lt;/p&gt;
&lt;p&gt;We want to emphasize that vault security was not impacted. At 1Password, protecting our customers’ privacy, passwords, and credentials is our highest priority.&lt;/p&gt;
&lt;p&gt;We’ll be publishing a postmortem covering what went wrong, the timeline, and the concrete changes we’re making to how we build and release future browser extension updates.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.1password.community/discussions/developers/1password-chrome-extension-is-incorrectly-manipulating--blocks/165639/replies/165982&quot;&gt;source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In the meantime, the problem is fixed on their end and an updated browser extension should be available shortly. However, there is no indication on which version actually contains the fix.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/html&quot;&gt;#html&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-01-05T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/be-aware-of-1password-breaking-syntax-highlighting</id>
    <title>🐥 Be aware of 1Password breaking syntax highlighting</title>
    <updated>2026-01-05T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/adding-a-second-css-and-js-bundle-to-a-phoenix-application"/>
    <content type="html">&lt;p&gt;Phoenix ships with a single asset pipeline by default, but real-world applications often need more. An admin area or backoffice is a common case where separate CSS and JS bundles keep concerns isolated. This post shows how to add a second bundle, including Tailwind configuration and dev-time watchers.&lt;/p&gt;
&lt;h2&gt;Default Phoenix asset setup recap&lt;/h2&gt;
&lt;p&gt;A standard Phoenix app includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;assets/js/app.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;assets/css/app.css&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tailwind and esbuild wired via &lt;code&gt;config/config.exs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Dev watchers for live rebuilding&lt;/li&gt;
&lt;li&gt;A single layout loading &lt;code&gt;app.css&lt;/code&gt; and &lt;code&gt;app.js&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We’ll extend this setup without affecting the default bundle.&lt;/p&gt;
&lt;h1&gt;Adding a second JavaScript entry point&lt;/h1&gt;
&lt;p&gt;Create a new JS entry:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;assets/js/admin.js
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable-builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;Admin bundle loaded&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This becomes the root of the admin bundle.&lt;/p&gt;
&lt;h1&gt;Adding a second CSS entry point&lt;/h1&gt;
&lt;p&gt;Create a new stylesheet:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;assets/css/admin.css
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For Tailwind:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-css&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-directive&quot;&gt;@tailwind&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; base&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword-directive&quot;&gt;@tailwind&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; components&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-directive&quot;&gt;@tailwind&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; utilities&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows full Tailwind usage without leaking styles into the main app.&lt;/p&gt;
&lt;h1&gt;Updating Tailwind build configuration&lt;/h1&gt;
&lt;p&gt;Open &lt;code&gt;config/config.exs&lt;/code&gt; and extend the &lt;code&gt;:tailwind&lt;/code&gt; config:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:tailwind&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;version: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;4.1.16&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;default: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;args: &lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;      --input=css/app.css
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      --output=../priv/static/assets/app.css
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;cd: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;expand&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;..&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant-builtin&quot;&gt;__DIR__&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;admin: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;args: &lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;      --input=css/admin.css
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;      --output=../priv/static/assets/admin.css
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;cd: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;expand&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;..&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant-builtin&quot;&gt;__DIR__&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You now have two independent Tailwind builds.&lt;/p&gt;
&lt;h1&gt;Updating esbuild configuration&lt;/h1&gt;
&lt;p&gt;Still in &lt;code&gt;config/config.exs&lt;/code&gt;, add a second esbuild profile:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:esbuild&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;version: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;0.25.11&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;default: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;args:
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;/span&gt;      &lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;js/app.js --bundle --target=es2022 --outdir=../priv/static/assets --external:/fonts/* --external:/images/* --alias:@=.&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;cd: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;expand&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;../assets&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant-builtin&quot;&gt;__DIR__&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;env: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;NODE_PATH&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;expand&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;../deps&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant-builtin&quot;&gt;__DIR__&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Mix.Project&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;build_path&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;admin: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;args:
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;/span&gt;      &lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;js/admin.js --bundle --target=es2022 --outdir=../priv/static/assets --external:/fonts/* --external:/images/* --alias:@=.&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;cd: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;expand&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;../assets&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant-builtin&quot;&gt;__DIR__&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;env: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;NODE_PATH&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;expand&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;../deps&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;constant-builtin&quot;&gt;__DIR__&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Mix.Project&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;build_path&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Wiring both bundles into Mix aliases&lt;/h1&gt;
&lt;p&gt;Ensure both Tailwind and esbuild profiles run during deployment by editing &lt;code&gt;mix.exs&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;aliases&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;&quot;assets.deploy&quot;: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;tailwind default --minify&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;tailwind admin --minify&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;esbuild default --minify&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;esbuild admin --minify&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;string&quot;&gt;&quot;phx.digest&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Adding dev watchers&lt;/h1&gt;
&lt;p&gt;Without watchers, the second bundle won’t rebuild in development. Update &lt;code&gt;config/dev.exs&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:my_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyAppWeb.Endpoint&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;watchers: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;esbuild: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Esbuild&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:install_and_run&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:default&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;--sourcemap=inline --watch&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;esbuild_admin: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Esbuild&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:install_and_run&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:admin&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;--sourcemap=inline --watch&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;tailwind: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Tailwind&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:install_and_run&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:default&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;--watch&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;tailwind_admin: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Tailwind&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:install_and_run&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:admin&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;--watch&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each watcher maps cleanly to a build profile.&lt;/p&gt;
&lt;h1&gt;Referencing the admin assets in a layout&lt;/h1&gt;
&lt;p&gt;Include the admin bundle:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-heex&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;link&lt;/span&gt; &lt;span class=&quot;tag-attribute&quot;&gt;phx-track-static&lt;/span&gt; &lt;span class=&quot;tag-attribute&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;stylesheet&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;tag-attribute&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;p&quot;/assets/admin.css&quot;&lt;/span&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;tag-delimiter&quot;&gt;/&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;script&lt;/span&gt; &lt;span class=&quot;tag-attribute&quot;&gt;defer&lt;/span&gt; &lt;span class=&quot;tag-attribute&quot;&gt;phx-track-static&lt;/span&gt; &lt;span class=&quot;tag-attribute&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;text/javascript&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;tag-attribute&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;p&quot;/assets/admin.js&quot;&lt;/span&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Why this setup works well&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Tailwind scanning stays fast and precise&lt;/li&gt;
&lt;li&gt;Admin styles and JS are fully isolated&lt;/li&gt;
&lt;li&gt;Dev experience remains identical to the default setup&lt;/li&gt;
&lt;li&gt;Adding a third bundle follows the same pattern&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach fits naturally into Phoenix’s asset pipeline while keeping growth manageable.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/javascript&quot;&gt;#javascript&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/css&quot;&gt;#css&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/html&quot;&gt;#html&lt;/a&gt;&lt;/p&gt;</content>
    <published>2025-12-31T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/adding-a-second-css-and-js-bundle-to-a-phoenix-application</id>
    <title>🐥 Adding a second CSS and JS bundle to a Phoenix application</title>
    <updated>2025-12-31T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/validating-webhook-signatures-in-phoenix"/>
    <content type="html">&lt;p&gt;When exposing a webhook endpoint, signature validation is essential. It ensures that incoming requests actually originate from the expected provider and that the payload has not been tampered with in transit.&lt;/p&gt;
&lt;p&gt;Phoenix provides all the building blocks needed to implement this cleanly and generically, without coupling your code to a specific webhook provider.&lt;/p&gt;
&lt;p&gt;This post shows a reusable pattern you can adapt to any HMAC-signed webhook.&lt;/p&gt;
&lt;h1&gt;The general webhook signature pattern&lt;/h1&gt;
&lt;p&gt;Most webhook providers follow a similar approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They send a signature in a request header&lt;/li&gt;
&lt;li&gt;The signature is an HMAC of the &lt;strong&gt;raw request body&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;A shared secret is used as the HMAC key&lt;/li&gt;
&lt;li&gt;The receiver must recompute the signature and compare it securely&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While header names and algorithms may differ, the structure remains the same.&lt;/p&gt;
&lt;h1&gt;Capturing the raw request body&lt;/h1&gt;
&lt;p&gt;Signature verification requires access to the raw request body, before JSON decoding occurs. Phoenix parses the body eagerly, so you need to explicitly capture it.&lt;/p&gt;
&lt;p&gt;Configure a custom body reader in your endpoint.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# endpoint.ex&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;plug&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Plug.Parsers&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;parsers: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:json&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;pass: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;json_decoder: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Jason&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;string-special-symbol&quot;&gt;body_reader: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;MyAppWeb.BodyReader&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:cache_raw_body&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyAppWeb.BodyReader&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;cache_raw_body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Plug.Conn&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;read_body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Plug.Conn&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;assign&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:raw_body&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The raw payload is now available as &lt;code&gt;conn.assigns[:raw_body]&lt;/code&gt; for later validation.&lt;/p&gt;
&lt;h1&gt;A generic signature validation plug&lt;/h1&gt;
&lt;p&gt;Instead of hardcoding provider-specific details, you can write a reusable plug that accepts configuration options such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Header name&lt;/li&gt;
&lt;li&gt;Hash algorithm&lt;/li&gt;
&lt;li&gt;Shared secret&lt;/li&gt;
&lt;li&gt;Optional encoding or prefix handling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Below is a minimal but flexible implementation for HMAC-based signatures.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyAppWeb.Plugs.WebhookSignature&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Plug.Conn&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;header: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Keyword&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;fetch!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:header&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;secret: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Keyword&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;fetch!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:secret&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;algorithm: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Keyword&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:algorithm&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:sha256&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;header: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;header&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;signature&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;-&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;get_req_header&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;header&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;         &lt;span class=&quot;variable&quot;&gt;raw_body&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;is_binary&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;raw_body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;-&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:raw_body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;         &lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;-&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;valid_signature?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;raw_body&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;signature&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;      &lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;      &lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;        &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:unauthorized&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Invalid signature&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;        &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;halt&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;valid_signature?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;signature&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;secret: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;algorithm: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;algorithm&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;expected&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;      &lt;span class=&quot;type&quot;&gt;:crypto&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;mac&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:hmac&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;algorithm&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;28&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;encode16&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;case: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:lower&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;29&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;30&quot;&gt;    &lt;span class=&quot;module&quot;&gt;Plug.Crypto&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;secure_compare&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;expected&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;signature&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;31&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;32&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This plug makes no assumptions about the webhook provider beyond the use of an HMAC.&lt;/p&gt;
&lt;h1&gt;Applying the plug per webhook endpoint&lt;/h1&gt;
&lt;p&gt;Different webhook providers can now be configured independently at the router level.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# router.ex&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;pipeline&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:webhook_provider_a&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;plug&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyAppWeb.Plugs.WebhookSignature&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;header: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;x-webhook-signature&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;secret: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;fetch_env!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:my_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:provider_a&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:secret&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;pipeline&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:webhook_provider_b&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;plug&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyAppWeb.Plugs.WebhookSignature&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;header: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;x-signature&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;secret: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;fetch_env!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:my_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:provider_b&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:secret&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;string-special-symbol&quot;&gt;algorithm: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:sha512&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;scope&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;/webhooks&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyAppWeb&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;pipe_through&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:api&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:webhook_provider_a&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;post&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;/provider-a&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;ProviderAController&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:create&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps validation close to routing and avoids leaking security concerns into controllers.&lt;/p&gt;
&lt;h1&gt;Keeping controllers focused&lt;/h1&gt;
&lt;p&gt;With signature validation handled by a plug, controllers can assume authenticity and focus purely on business logic.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;ok&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Common pitfalls&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Validating against parsed JSON instead of the raw request body&lt;/li&gt;
&lt;li&gt;Comparing signatures without a constant-time function&lt;/li&gt;
&lt;li&gt;Hardcoding secrets instead of injecting them via configuration&lt;/li&gt;
&lt;li&gt;Applying the plug after the request body has already been consumed&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Webhook signature validation is a cross-cutting concern that fits naturally into Phoenix plugs. By capturing the raw request body and using a configurable, generic validation plug, you can support multiple webhook providers with minimal duplication while keeping your controllers clean and secure.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/http&quot;&gt;#http&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
    <published>2025-12-30T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/validating-webhook-signatures-in-phoenix</id>
    <title>🐥 Validating webhook signatures in Phoenix</title>
    <updated>2025-12-30T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/using-the-lockf-command-on-linux-and-macos"/>
    <content type="html">&lt;p&gt;The &lt;code&gt;lockf&lt;/code&gt; command is a small but useful Unix utility for applying advisory file locks from the shell. It is commonly used in scripts to prevent multiple instances of a process from running at the same time.&lt;/p&gt;
&lt;h1&gt;What &lt;code&gt;lockf&lt;/code&gt; does&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;lockf&lt;/code&gt; applies a POSIX advisory lock on an open file descriptor. As long as the process holds the lock, other processes attempting to acquire a conflicting lock will either block or fail, depending on how &lt;code&gt;lockf&lt;/code&gt; is invoked.&lt;/p&gt;
&lt;p&gt;This is most often used for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Preventing concurrent cron jobs&lt;/li&gt;
&lt;li&gt;Serializing access to shared resources&lt;/li&gt;
&lt;li&gt;Implementing simple process-level mutexes in shell scripts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The locking is &lt;em&gt;advisory&lt;/em&gt;, meaning all cooperating processes must also use locking for it to be effective.&lt;/p&gt;
&lt;h1&gt;Basic usage&lt;/h1&gt;
&lt;p&gt;The most common pattern is wrapping a command:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;lockf&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/var/lock/myjob.lock&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;my_command&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Opens (and creates if needed) the lock file&lt;/li&gt;
&lt;li&gt;Acquires an exclusive lock&lt;/li&gt;
&lt;li&gt;Executes &lt;code&gt;my_command&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Releases the lock when the command exits&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Non-blocking locks&lt;/h1&gt;
&lt;p&gt;To fail immediately if the lock is already held, use &lt;code&gt;-n&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;lockf&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/var/lock/myjob.lock&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;my_command&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;function-builtin&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is ideal for cron jobs where you want to skip execution if another instance is still running.&lt;/p&gt;
&lt;h1&gt;Timeout-based locking&lt;/h1&gt;
&lt;p&gt;On Linux, you can specify a timeout with &lt;code&gt;-t&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;lockf&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-t&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/var/lock/myjob.lock&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;my_command&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This waits up to 10 seconds to acquire the lock before failing.&lt;/p&gt;
&lt;p&gt;Note: this option is not available on macOS.&lt;/p&gt;
&lt;h1&gt;Exit codes&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;lockf&lt;/code&gt; uses meaningful exit codes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt;: command executed successfully&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1&lt;/code&gt;: invalid arguments or usage error&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2&lt;/code&gt;: lock acquisition failed (for example with &lt;code&gt;-n&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes it easy to integrate into scripts with conditional logic.&lt;/p&gt;
&lt;h1&gt;Differences between Linux and macOS&lt;/h1&gt;
&lt;p&gt;While the core behavior is similar, there are a few differences worth knowing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Implementation&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux: typically part of &lt;code&gt;util-linux&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;macOS: BSD-derived implementation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Options&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux supports &lt;code&gt;-t&lt;/code&gt; (timeout)&lt;/li&gt;
&lt;li&gt;macOS does not support timeouts&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lock semantics&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Both use advisory locks backed by &lt;code&gt;fcntl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Locks are released automatically when the process exits or the file descriptor is closed&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For portable scripts, avoid Linux-only options like &lt;code&gt;-t&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Common pattern for scripts&lt;/h1&gt;
&lt;p&gt;A widely used idiom looks like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-directive&quot;&gt;#!/bin/sh&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;variable&quot;&gt;lockfile&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;/tmp/myjob.lock&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;lockf&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;lockfile&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;sh&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  echo &quot;Running job&quot;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  sleep 30
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures only one instance of the script runs at a time, regardless of how often it is triggered.&lt;/p&gt;
&lt;h1&gt;When not to use &lt;code&gt;lockf&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;lockf&lt;/code&gt; is not suitable when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need mandatory locking&lt;/li&gt;
&lt;li&gt;Locks must survive process crashes or reboots&lt;/li&gt;
&lt;li&gt;Coordination is required across NFS in unreliable setups&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In those cases, higher-level coordination mechanisms or external systems are a better fit.&lt;/p&gt;
&lt;h1&gt;Summary&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;lockf&lt;/code&gt; is a simple and effective tool for process synchronization in shell scripts. When used carefully, it provides a clean solution to avoid concurrent execution on both Linux and macOS, with only minor portability considerations.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/terminal&quot;&gt;#terminal&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/linux&quot;&gt;#linux&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/mac&quot;&gt;#mac&lt;/a&gt;&lt;/p&gt;</content>
    <published>2025-12-29T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/using-the-lockf-command-on-linux-and-macos</id>
    <title>🐥 Using the lockf command on Linux and macOS</title>
    <updated>2025-12-29T18:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/til-removing-old-php-versions-after-an-upgrade"/>
    <content type="html">&lt;p&gt;After upgrading PHP on a Debian or Ubuntu system, it’s a good idea to clean up old PHP versions once you’ve confirmed the new installation works correctly. This helps reduce clutter and avoids accidentally running outdated binaries or services.&lt;/p&gt;
&lt;p&gt;You can remove all packages belonging to a specific PHP version using &lt;code&gt;apt purge&lt;/code&gt; with a wildcard:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;purge&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;^php8.3.*&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command purges all installed PHP 8.3 packages, including configuration files. Adjust the version number to match the PHP release you want to remove.&lt;/p&gt;
&lt;p&gt;As a final step, you may want to run:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;autoremove&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to clean up any remaining unused dependencies.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/terminal&quot;&gt;#terminal&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/linux&quot;&gt;#linux&lt;/a&gt;&lt;/p&gt;</content>
    <published>2025-12-23T18:00:00Z</published>
    <id>https://www.yellowduck.be/posts/til-removing-old-php-versions-after-an-upgrade</id>
    <title>🐥 TIL: Removing old PHP versions after an upgrade</title>
    <updated>2025-12-23T18:00:00Z</updated>
  </entry>
</feed>